2019-05-24 11:29:27 -07:00
//
2019-07-01 15:19:22 -07:00
// A L T D e v i c e M a n a g e r + I n s t a l l a t i o n . s w i f t
2019-05-24 11:29:27 -07:00
// A l t S e r v e r
//
2019-07-01 15:19:22 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 7 / 1 / 1 9 .
2019-05-24 11:29:27 -07:00
// C o p y r i g h t © 2 0 1 9 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 Cocoa
2019-07-01 15:19:22 -07:00
import UserNotifications
2019-11-18 14:17:57 -08:00
import ObjectiveC
2019-05-24 11:29:27 -07:00
2019-11-13 11:35:37 -08:00
#if STAGING
private let appURL = URL ( string : " https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa " ) !
#else
private let appURL = URL ( string : " https://f000.backblazeb2.com/file/altstore/altstore.ipa " ) !
#endif
2019-09-14 11:28:57 -07:00
enum InstallError : LocalizedError
2019-05-29 15:50:53 -07:00
{
2019-09-14 11:28:57 -07:00
case cancelled
2019-05-29 15:50:53 -07:00
case noTeam
case missingPrivateKey
case missingCertificate
2019-09-14 11:28:57 -07:00
var errorDescription : String ? {
2019-05-29 15:50:53 -07:00
switch self
{
2019-09-14 11:28:57 -07:00
case . cancelled : return NSLocalizedString ( " The operation was cancelled. " , comment : " " )
2019-05-29 15:50:53 -07:00
case . noTeam : return " You are not a member of any developer teams. "
case . missingPrivateKey : return " The developer certificate's private key could not be found. "
case . missingCertificate : return " The developer certificate could not be found. "
}
}
}
2019-05-24 11:29:27 -07:00
2019-07-01 15:19:22 -07:00
extension ALTDeviceManager
2019-05-29 15:50:53 -07:00
{
2019-07-01 15:19:22 -07:00
func installAltStore ( to device : ALTDevice , appleID : String , password : String , completion : @ escaping ( Result < Void , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-06-25 13:34:12 -07:00
let destinationDirectoryURL = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID ( ) . uuidString )
func finish ( _ error : Error ? , title : String = " " )
2019-05-29 15:50:53 -07:00
{
DispatchQueue . main . async {
2019-06-25 13:34:12 -07:00
if let error = error
{
2019-07-01 15:19:22 -07:00
completion ( . failure ( error ) )
2019-06-25 13:34:12 -07:00
}
else
{
2019-07-01 15:19:22 -07:00
completion ( . success ( ( ) ) )
2019-06-25 13:34:12 -07:00
}
2019-05-29 15:50:53 -07:00
}
2019-06-25 13:34:12 -07:00
try ? FileManager . default . removeItem ( at : destinationDirectoryURL )
}
2019-11-18 14:17:57 -08:00
AnisetteDataManager . shared . requestAnisetteData { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let anisetteData = try result . get ( )
2019-07-01 15:19:22 -07:00
2019-11-18 14:17:57 -08:00
self . authenticate ( appleID : appleID , password : password , anisetteData : anisetteData ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let ( account , session ) = try result . get ( )
2019-05-29 15:50:53 -07:00
2019-11-18 14:17:57 -08:00
self . fetchTeam ( for : account , session : session ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let team = try result . get ( )
2019-05-29 15:50:53 -07:00
2019-11-18 14:17:57 -08:00
self . register ( device , team : team , session : session ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let device = try result . get ( )
2019-05-29 15:50:53 -07:00
2019-11-18 14:17:57 -08:00
self . fetchCertificate ( for : team , session : session ) { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let certificate = try result . get ( )
2019-06-26 17:05:52 -07:00
2019-11-18 14:17:57 -08:00
let content = UNMutableNotificationContent ( )
content . title = String ( format : NSLocalizedString ( " Installing AltStore to %@... " , comment : " " ) , device . name )
content . body = NSLocalizedString ( " This may take a few seconds. " , comment : " " )
2019-06-25 13:34:12 -07:00
2019-11-18 14:17:57 -08:00
let request = UNNotificationRequest ( identifier : UUID ( ) . uuidString , content : content , trigger : nil )
UNUserNotificationCenter . current ( ) . add ( request )
2019-06-26 17:05:52 -07:00
2019-11-18 14:17:57 -08:00
self . downloadApp { ( result ) in
2019-05-29 15:50:53 -07:00
do
{
2019-11-18 14:17:57 -08:00
let fileURL = try result . get ( )
try FileManager . default . createDirectory ( at : destinationDirectoryURL , withIntermediateDirectories : true , attributes : nil )
2019-06-25 13:34:12 -07:00
2019-11-18 14:17:57 -08:00
let appBundleURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : destinationDirectoryURL )
do
{
try FileManager . default . removeItem ( at : fileURL )
}
catch
{
print ( " Failed to remove downloaded .ipa. " , error )
}
guard let application = ALTApplication ( fileURL : appBundleURL ) else { throw ALTError ( . invalidApp ) }
self . registerAppID ( name : " AltStore " , identifier : " com.rileytestut.AltStore " , team : team , session : session ) { ( result ) in
2019-06-25 13:34:12 -07:00
do
{
2019-06-26 17:05:52 -07:00
let appID = try result . get ( )
2019-06-25 13:34:12 -07:00
2019-11-18 14:17:57 -08:00
self . updateFeatures ( for : appID , app : application , team : team , session : session ) { ( result ) in
2019-06-26 17:05:52 -07:00
do
{
2019-11-18 14:17:57 -08:00
let appID = try result . get ( )
2019-06-26 17:05:52 -07:00
2019-11-18 14:17:57 -08:00
self . fetchProvisioningProfile ( for : appID , team : team , session : session ) { ( result ) in
do
{
let provisioningProfile = try result . get ( )
self . install ( application , to : device , team : team , appID : appID , certificate : certificate , profile : provisioningProfile ) { ( result ) in
finish ( result . error , title : " Failed to Install AltStore " )
}
}
catch
{
finish ( error , title : " Failed to Fetch Provisioning Profile " )
}
2019-06-26 17:05:52 -07:00
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Update App ID " )
2019-06-26 17:05:52 -07:00
}
2019-06-25 13:34:12 -07:00
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Register App " )
2019-06-25 13:34:12 -07:00
}
}
2019-05-29 15:50:53 -07:00
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Download AltStore " )
return
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Fetch Certificate " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Register Device " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Fetch Team " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Authenticate " )
2019-05-29 15:50:53 -07:00
}
}
}
catch
{
2019-11-18 14:17:57 -08:00
finish ( error , title : " Failed to Fetch Anisette Data " )
2019-05-29 15:50:53 -07:00
}
}
}
2019-06-26 17:05:52 -07:00
func downloadApp ( completionHandler : @ escaping ( Result < URL , Error > ) -> Void )
{
let downloadTask = URLSession . shared . downloadTask ( with : appURL ) { ( fileURL , response , error ) in
do
{
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
completionHandler ( . success ( fileURL ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
downloadTask . resume ( )
}
2019-11-18 14:17:57 -08:00
func authenticate ( appleID : String , password : String , anisetteData : ALTAnisetteData , completionHandler : @ escaping ( Result < ( ALTAccount , ALTAppleAPISession ) , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-11-18 14:17:57 -08:00
func handleVerificationCode ( _ completionHandler : @ escaping ( String ? ) -> Void )
{
DispatchQueue . main . async {
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " Two-Factor Authentication Enabled " , comment : " " )
alert . informativeText = NSLocalizedString ( " Please enter the 6-digit verification code that was sent to your Apple devices. " , comment : " " )
let textField = NSTextField ( frame : NSRect ( x : 0 , y : 0 , width : 300 , height : 22 ) )
textField . delegate = self
textField . translatesAutoresizingMaskIntoConstraints = false
textField . placeholderString = NSLocalizedString ( " 123456 " , comment : " " )
alert . accessoryView = textField
alert . window . initialFirstResponder = textField
alert . addButton ( withTitle : NSLocalizedString ( " Continue " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
self . securityCodeAlert = alert
self . securityCodeTextField = textField
self . validate ( )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
let response = alert . runModal ( )
if response = = . alertFirstButtonReturn
{
let code = textField . stringValue
completionHandler ( code )
}
else
{
completionHandler ( nil )
}
}
}
ALTAppleAPI . shared . authenticate ( appleID : appleID , password : password , anisetteData : anisetteData , verificationHandler : handleVerificationCode ) { ( account , session , error ) in
if let account = account , let session = session
{
completionHandler ( . success ( ( account , session ) ) )
}
else
{
completionHandler ( . failure ( error ? ? ALTAppleAPIError ( . unknown ) ) )
}
2019-05-29 15:50:53 -07:00
}
}
2019-11-18 14:17:57 -08:00
func fetchTeam ( for account : ALTAccount , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTTeam , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-09-27 14:29:23 -07:00
func finish ( _ result : Result < ALTTeam , Error > )
{
switch result
{
case . failure ( let error ) :
completionHandler ( . failure ( error ) )
case . success ( let team ) :
var isCancelled = false
if team . type != . free
{
DispatchQueue . main . sync {
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " Installing AltStore will revoke your iOS development certificate. " , comment : " " )
alert . informativeText = NSLocalizedString ( " " "
This will not affect apps you ' ve submitted to the App Store , but may cause apps you ' ve installed to your devices with Xcode to stop working until you reinstall them .
To prevent this from happening , feel free to try again with another Apple ID to install AltStore .
" " " , comment: " " )
alert . addButton ( withTitle : NSLocalizedString ( " Continue " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
let buttonIndex = alert . runModal ( )
if buttonIndex = = NSApplication . ModalResponse . alertSecondButtonReturn
{
isCancelled = true
}
}
if isCancelled
{
return completionHandler ( . failure ( InstallError . cancelled ) )
}
}
completionHandler ( . success ( team ) )
}
}
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchTeams ( for : account , session : session ) { ( teams , error ) in
2019-05-29 15:50:53 -07:00
do
{
let teams = try Result ( teams , error ) . get ( )
2019-09-13 14:26:21 -07:00
if let team = teams . first ( where : { $0 . type = = . free } )
{
2019-09-27 14:29:23 -07:00
return finish ( . success ( team ) )
2019-09-13 14:26:21 -07:00
}
else if let team = teams . first ( where : { $0 . type = = . individual } )
{
2019-09-27 14:29:23 -07:00
return finish ( . success ( team ) )
2019-09-13 14:26:21 -07:00
}
else if let team = teams . first
{
2019-09-27 14:29:23 -07:00
return finish ( . success ( team ) )
2019-09-13 14:26:21 -07:00
}
else
{
throw InstallError . noTeam
}
2019-05-29 15:50:53 -07:00
}
catch
{
2019-09-27 14:29:23 -07:00
finish ( . failure ( error ) )
2019-05-29 15:50:53 -07:00
}
}
}
2019-11-18 14:17:57 -08:00
func fetchCertificate ( for team : ALTTeam , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTCertificate , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchCertificates ( for : team , session : session ) { ( certificates , error ) in
2019-05-29 15:50:53 -07:00
do
{
let certificates = try Result ( certificates , error ) . get ( )
2019-09-14 11:28:57 -07:00
// C h e c k i f t h e r e i s a n o t h e r A l t S t o r e c e r t i f i c a t e , w h i c h m e a n s A l t S t o r e h a s b e e n i n s t a l l e d w i t h t h i s A p p l e I D b e f o r e .
if certificates . contains ( where : { $0 . machineName ? . starts ( with : " AltStore " ) = = true } )
{
var isCancelled = false
DispatchQueue . main . sync {
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " AltStore already installed on another device. " , comment : " " )
alert . informativeText = NSLocalizedString ( " Apps installed with AltStore on your other devices will stop working. Are you sure you want to continue? " , comment : " " )
alert . addButton ( withTitle : NSLocalizedString ( " Continue " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
let buttonIndex = alert . runModal ( )
if buttonIndex = = NSApplication . ModalResponse . alertSecondButtonReturn
{
isCancelled = true
}
}
if isCancelled
{
return completionHandler ( . failure ( InstallError . cancelled ) )
}
}
2019-05-29 15:50:53 -07:00
if let certificate = certificates . first
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . revoke ( certificate , for : team , session : session ) { ( success , error ) in
2019-05-29 15:50:53 -07:00
do
{
try Result ( success , error ) . get ( )
2019-11-18 14:17:57 -08:00
self . fetchCertificate ( for : team , session : session , completionHandler : completionHandler )
2019-05-29 15:50:53 -07:00
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
else
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . addCertificate ( machineName : " AltStore " , to : team , session : session ) { ( certificate , error ) in
2019-05-29 15:50:53 -07:00
do
{
let certificate = try Result ( certificate , error ) . get ( )
guard let privateKey = certificate . privateKey else { throw InstallError . missingPrivateKey }
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchCertificates ( for : team , session : session ) { ( certificates , error ) in
2019-05-29 15:50:53 -07:00
do
{
let certificates = try Result ( certificates , error ) . get ( )
2019-06-18 17:40:30 -07:00
guard let certificate = certificates . first ( where : { $0 . serialNumber = = certificate . serialNumber } ) else {
2019-05-29 15:50:53 -07:00
throw InstallError . missingCertificate
}
certificate . privateKey = privateKey
completionHandler ( . success ( certificate ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-11-18 14:17:57 -08:00
func registerAppID ( name appName : String , identifier : String , team : ALTTeam , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTAppID , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-05-31 18:24:08 -07:00
let bundleID = " com. \( team . identifier ) . \( identifier ) "
2019-05-29 15:50:53 -07:00
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchAppIDs ( for : team , session : session ) { ( appIDs , error ) in
2019-05-29 15:50:53 -07:00
do
{
let appIDs = try Result ( appIDs , error ) . get ( )
if let appID = appIDs . first ( where : { $0 . bundleIdentifier = = bundleID } )
{
completionHandler ( . success ( appID ) )
}
else
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . addAppID ( withName : appName , bundleIdentifier : bundleID , team : team , session : session ) { ( appID , error ) in
2019-05-29 15:50:53 -07:00
completionHandler ( Result ( appID , error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-11-18 14:17:57 -08:00
func updateFeatures ( for appID : ALTAppID , app : ALTApplication , team : ALTTeam , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTAppID , Error > ) -> Void )
2019-06-25 13:34:12 -07:00
{
let requiredFeatures = app . entitlements . compactMap { ( entitlement , value ) -> ( ALTFeature , Any ) ? in
2019-09-30 13:59:17 -07:00
guard let feature = ALTFeature ( entitlement : entitlement ) else { return nil }
2019-06-25 13:34:12 -07:00
return ( feature , value )
}
var features = requiredFeatures . reduce ( into : [ ALTFeature : Any ] ( ) ) { $0 [ $1 . 0 ] = $1 . 1 }
if let applicationGroups = app . entitlements [ . appGroups ] as ? [ String ] , ! applicationGroups . isEmpty
{
features [ . appGroups ] = true
}
let appID = appID . copy ( ) as ! ALTAppID
appID . features = features
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . update ( appID , team : team , session : session ) { ( appID , error ) in
2019-06-25 13:34:12 -07:00
completionHandler ( Result ( appID , error ) )
}
}
2019-11-18 14:17:57 -08:00
func register ( _ device : ALTDevice , team : ALTTeam , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTDevice , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchDevices ( for : team , session : session ) { ( devices , error ) in
2019-05-29 15:50:53 -07:00
do
{
let devices = try Result ( devices , error ) . get ( )
if let device = devices . first ( where : { $0 . identifier = = device . identifier } )
{
completionHandler ( . success ( device ) )
}
else
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . registerDevice ( name : device . name , identifier : device . identifier , team : team , session : session ) { ( device , error ) in
2019-05-29 15:50:53 -07:00
completionHandler ( Result ( device , error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2019-11-18 14:17:57 -08:00
func fetchProvisioningProfile ( for appID : ALTAppID , team : ALTTeam , session : ALTAppleAPISession , completionHandler : @ escaping ( Result < ALTProvisioningProfile , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-11-18 14:17:57 -08:00
ALTAppleAPI . shared . fetchProvisioningProfile ( for : appID , team : team , session : session ) { ( profile , error ) in
2019-05-29 15:50:53 -07:00
completionHandler ( Result ( profile , error ) )
}
}
2019-06-25 13:34:12 -07:00
func install ( _ application : ALTApplication , to device : ALTDevice , team : ALTTeam , appID : ALTAppID , certificate : ALTCertificate , profile : ALTProvisioningProfile , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
2019-05-29 15:50:53 -07:00
{
2019-06-25 13:34:12 -07:00
DispatchQueue . global ( ) . async {
do
{
let infoPlistURL = application . fileURL . appendingPathComponent ( " Info.plist " )
guard var infoDictionary = NSDictionary ( contentsOf : infoPlistURL ) as ? [ String : Any ] else { throw ALTError ( . missingInfoPlist ) }
infoDictionary [ kCFBundleIdentifierKey as String ] = profile . bundleIdentifier
infoDictionary [ Bundle . Info . deviceID ] = device . identifier
2019-08-01 10:45:54 -07:00
infoDictionary [ Bundle . Info . serverID ] = UserDefaults . standard . serverID
2019-10-28 12:53:56 -07:00
infoDictionary [ Bundle . Info . certificateID ] = certificate . serialNumber
2019-06-25 13:34:12 -07:00
try ( infoDictionary as NSDictionary ) . write ( to : infoPlistURL )
2019-10-28 12:53:56 -07:00
if
let machineIdentifier = certificate . machineIdentifier ,
let encryptedData = certificate . encryptedP12Data ( withPassword : machineIdentifier )
{
let certificateURL = application . fileURL . appendingPathComponent ( " ALTCertificate.p12 " )
try encryptedData . write ( to : certificateURL , options : . atomic )
}
2019-06-25 13:34:12 -07:00
let resigner = ALTSigner ( team : team , certificate : certificate )
resigner . signApp ( at : application . fileURL , provisioningProfiles : [ profile ] ) { ( success , error ) in
do
{
try Result ( success , error ) . get ( )
ALTDeviceManager . shared . installApp ( at : application . fileURL , toDeviceWithUDID : device . identifier ) { ( success , error ) in
completionHandler ( Result ( success , error ) )
}
}
catch
{
print ( " Failed to install app " , error )
completionHandler ( . failure ( error ) )
2019-05-29 15:50:53 -07:00
}
}
}
2019-06-25 13:34:12 -07:00
catch
{
print ( " Failed to install AltStore " , error )
completionHandler ( . failure ( error ) )
}
2019-05-29 15:50:53 -07:00
}
2019-05-24 11:29:27 -07:00
}
}
2019-11-18 14:17:57 -08:00
private var securityCodeAlertKey = 0
private var securityCodeTextFieldKey = 0
extension ALTDeviceManager : NSTextFieldDelegate
{
var securityCodeAlert : NSAlert ? {
get { return objc_getAssociatedObject ( self , & securityCodeAlertKey ) as ? NSAlert }
set { objc_setAssociatedObject ( self , & securityCodeAlertKey , newValue , . OBJC_ASSOCIATION_RETAIN_NONATOMIC ) }
}
var securityCodeTextField : NSTextField ? {
get { return objc_getAssociatedObject ( self , & securityCodeTextFieldKey ) as ? NSTextField }
set { objc_setAssociatedObject ( self , & securityCodeTextFieldKey , newValue , . OBJC_ASSOCIATION_RETAIN_NONATOMIC ) }
}
public func controlTextDidChange ( _ obj : Notification )
{
self . validate ( )
}
public func controlTextDidEndEditing ( _ obj : Notification )
{
self . validate ( )
}
private func validate ( )
{
guard let code = self . securityCodeTextField ? . stringValue . trimmingCharacters ( in : . whitespacesAndNewlines ) else { return }
if code . count = = 6
{
self . securityCodeAlert ? . buttons . first ? . isEnabled = true
}
else
{
self . securityCodeAlert ? . buttons . first ? . isEnabled = false
}
self . securityCodeAlert ? . layout ( )
}
}