mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Assuming the certificate used to originally sign an app is still valid, we can refresh an app simply by installing new provisioning profiles. However, if the signing certificate is no longer valid, we fall back to the old method of resigning + reinstalling.
226 lines
9.3 KiB
Swift
226 lines
9.3 KiB
Swift
//
|
|
// ResignAppOperation.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 6/7/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Roxas
|
|
|
|
import AltSign
|
|
|
|
@objc(ResignAppOperation)
|
|
class ResignAppOperation: ResultOperation<ALTApplication>
|
|
{
|
|
let context: InstallAppOperationContext
|
|
|
|
init(context: InstallAppOperationContext)
|
|
{
|
|
self.context = context
|
|
|
|
super.init()
|
|
|
|
self.progress.totalUnitCount = 3
|
|
}
|
|
|
|
override func main()
|
|
{
|
|
super.main()
|
|
|
|
if let error = self.context.error
|
|
{
|
|
self.finish(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard
|
|
let app = self.context.app,
|
|
let profiles = self.context.provisioningProfiles,
|
|
let team = self.context.team,
|
|
let certificate = self.context.certificate
|
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
|
|
|
// Prepare app bundle
|
|
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
|
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
|
|
|
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
|
|
guard let appBundleURL = self.process(result) else { return }
|
|
|
|
print("Resigning App:", self.context.bundleIdentifier)
|
|
|
|
// Resign app bundle
|
|
let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in
|
|
guard let resignedURL = self.process(result) else { return }
|
|
|
|
// Finish
|
|
do
|
|
{
|
|
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
|
|
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
|
|
|
|
// Use appBundleURL since we need an app bundle, not .ipa.
|
|
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
|
self.finish(.success(resignedApplication))
|
|
}
|
|
catch
|
|
{
|
|
self.finish(.failure(error))
|
|
}
|
|
}
|
|
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
|
}
|
|
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
|
}
|
|
|
|
func process<T>(_ result: Result<T, Error>) -> T?
|
|
{
|
|
switch result
|
|
{
|
|
case .failure(let error):
|
|
self.finish(.failure(error))
|
|
return nil
|
|
|
|
case .success(let value):
|
|
guard !self.isCancelled else {
|
|
self.finish(.failure(OperationError.cancelled))
|
|
return nil
|
|
}
|
|
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension ResignAppOperation
|
|
{
|
|
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
|
{
|
|
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
|
|
|
let bundleIdentifier = app.bundleIdentifier
|
|
let openURL = InstalledApp.openAppURL(for: app)
|
|
|
|
let fileURL = app.fileURL
|
|
|
|
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
|
|
{
|
|
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
|
|
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
|
|
guard var infoDictionary = bundle.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
|
|
|
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
|
|
|
for (key, value) in additionalInfoDictionaryValues
|
|
{
|
|
infoDictionary[key] = value
|
|
}
|
|
|
|
if let appGroups = profile.entitlements[.appGroups] as? [String]
|
|
{
|
|
infoDictionary[Bundle.Info.appGroups] = appGroups
|
|
}
|
|
|
|
// Add app-specific exported UTI so we can check later if this app (extension) is installed or not.
|
|
let installedAppUTI = ["UTTypeConformsTo": [],
|
|
"UTTypeDescription": "AltStore Installed App",
|
|
"UTTypeIconFiles": [],
|
|
"UTTypeIdentifier": InstalledApp.installedAppUTI(forBundleIdentifier: profile.bundleIdentifier),
|
|
"UTTypeTagSpecification": [:]] as [String : Any]
|
|
|
|
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
|
|
exportedUTIs.append(installedAppUTI)
|
|
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
|
|
|
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
|
}
|
|
|
|
DispatchQueue.global().async {
|
|
do
|
|
{
|
|
let appBundleURL = self.context.temporaryDirectory.appendingPathComponent("App.app")
|
|
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
|
|
|
// Become current so we can observe progress from unzipAppBundle().
|
|
progress.becomeCurrent(withPendingUnitCount: 1)
|
|
|
|
guard let appBundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
|
|
guard let infoDictionary = appBundle.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
|
|
|
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
|
|
|
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
|
"CFBundleURLName": bundleIdentifier,
|
|
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
|
|
allURLSchemes.append(altstoreURLScheme)
|
|
|
|
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
|
|
|
|
if self.context.bundleIdentifier == StoreApp.altstoreAppID || self.context.bundleIdentifier == StoreApp.alternativeAltStoreAppID
|
|
{
|
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
|
|
additionalValues[Bundle.Info.deviceID] = udid
|
|
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID
|
|
|
|
if
|
|
let data = Keychain.shared.signingCertificate,
|
|
let signingCertificate = ALTCertificate(p12Data: data, password: nil),
|
|
let encryptingPassword = Keychain.shared.signingCertificatePassword
|
|
{
|
|
additionalValues[Bundle.Info.certificateID] = signingCertificate.serialNumber
|
|
|
|
let encryptedData = signingCertificate.encryptedP12Data(withPassword: encryptingPassword)
|
|
try encryptedData?.write(to: appBundle.certificateURL, options: .atomic)
|
|
}
|
|
else
|
|
{
|
|
// The embedded certificate + certificate identifier are already in app bundle, no need to update them.
|
|
}
|
|
}
|
|
|
|
// Prepare app
|
|
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
|
|
|
|
if let directory = appBundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
|
{
|
|
for case let fileURL as URL in enumerator
|
|
{
|
|
guard let appExtension = Bundle(url: fileURL) else { throw ALTError(.missingAppBundle) }
|
|
try prepare(appExtension)
|
|
}
|
|
}
|
|
|
|
completionHandler(.success(appBundleURL))
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
|
|
return progress
|
|
}
|
|
|
|
func resignAppBundle(at fileURL: URL, team: ALTTeam, certificate: ALTCertificate, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
|
{
|
|
let signer = ALTSigner(team: team, certificate: certificate)
|
|
let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in
|
|
do
|
|
{
|
|
try Result(success, error).get()
|
|
|
|
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
|
|
completionHandler(.success(ipaURL))
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
|
|
return progress
|
|
}
|
|
}
|