diff --git a/AltServer/ViewController.swift b/AltServer/ViewController.swift index ee13167c..7c9cbfa6 100644 --- a/AltServer/ViewController.swift +++ b/AltServer/ViewController.swift @@ -355,7 +355,7 @@ private extension ViewController let zippedURL = try FileManager.default.zipAppBundle(at: appBundleURL) let resigner = ALTSigner(team: team, certificate: certificate) - resigner.signApp(at: zippedURL, provisioningProfile: profile) { (success, error) in + resigner.signApp(at: zippedURL, provisioningProfiles: [profile]) { (success, error) in do { try Result(success, error).get() diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 03ee2bf8..aefd60dd 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -60,50 +60,53 @@ class ResignAppOperation: ResultOperation self.registerCurrentDevice(for: signer.team) { (result) in guard let _ = self.process(result) else { return } - // Register App + // Prepare Provisioning Profiles appContext.perform { - self.register(installedApp.app, team: signer.team) { (result) in - guard let appID = self.process(result) else { return } + self.prepareProvisioningProfiles(installedApp.fileURL, team: signer.team) { (result) in + guard let profiles = self.process(result) else { return } - // Fetch Provisioning Profile - self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in - guard let profile = self.process(result) else { return } + // Prepare app bundle + appContext.perform { + let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) + self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) - // Prepare app bundle - appContext.perform { - let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) - self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) + let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp, profiles: profiles) { (result) in + guard let appBundleURL = self.process(result) else { return } - let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp) { (result) in - guard let appBundleURL = self.process(result) else { return } + print("Resigning App:", appIdentifier) + + // Resign app bundle + let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in + guard let resignedURL = self.process(result) else { return } - print("Resigning App:", appIdentifier) - - // Resign app bundle - let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in - guard let resignedURL = self.process(result) else { return } - - // Finish - appContext.perform { - do + // Finish + appContext.perform { + do + { + installedApp.refreshedDate = Date() + + if let profile = profiles[installedApp.app.identifier] { installedApp.expirationDate = profile.expirationDate - installedApp.refreshedDate = Date() - - try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true) - - self.finish(.success(installedApp.refreshedIPAURL)) } - catch + else { - self.finish(.failure(error)) + installedApp.expirationDate = installedApp.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) } + + try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true) + + self.finish(.success(installedApp.refreshedIPAURL)) + } + catch + { + self.finish(.failure(error)) } } - prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) } - prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) + prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) } + prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) } } } @@ -172,10 +175,98 @@ private extension ResignAppOperation } } - func register(_ app: App, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void) + { + guard let bundle = Bundle(url: fileURL), let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) } + + let dispatchGroup = DispatchGroup() + + var profiles = [String: ALTProvisioningProfile]() + var error: Error? + + dispatchGroup.enter() + + self.prepareProvisioningProfile(for: app, team: team) { (result) in + switch result + { + case .failure(let e): error = e + case .success(let profile): + profiles[app.bundleIdentifier] = profile + } + dispatchGroup.leave() + } + + if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) + { + for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex" + { + guard let appExtension = ALTApplication(fileURL: fileURL) else { continue } + + dispatchGroup.enter() + + self.prepareProvisioningProfile(for: appExtension, team: team) { (result) in + switch result + { + case .failure(let e): error = e + case .success(let profile): + profiles[appExtension.bundleIdentifier] = profile + } + dispatchGroup.leave() + } + } + } + + dispatchGroup.notify(queue: .global()) { + if let error = error + { + completionHandler(.failure(error)) + } + else + { + completionHandler(.success(profiles)) + } + } + } + + func prepareProvisioningProfile(for app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + { + // Register + self.register(app, team: team) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Update features + self.updateFeatures(for: appID, app: app, team: team) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Update app groups + self.updateAppGroups(for: appID, app: app, team: team) { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let appID): + + // Fetch Provisioning Profile + self.fetchProvisioningProfile(for: appID, team: team) { (result) in + completionHandler(result) + } + } + } + } + } + } + } + } + + func register(_ app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) { let appName = app.name - let bundleID = "com.\(team.identifier).\(app.identifier)" + let bundleID = "com.\(team.identifier).\(app.bundleIdentifier)" ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in do @@ -200,6 +291,76 @@ private extension ResignAppOperation } } + func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + { + let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in + guard let feature = ALTFeature(entitlement) else { return nil } + 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 + + ALTAppleAPI.shared.update(appID, team: team) { (appID, error) in + completionHandler(Result(appID, error)) + } + } + + func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, completionHandler: @escaping (Result) -> Void) + { + // TODO: Handle apps belonging to more than one app group. + guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { + return completionHandler(.success(appID)) + } + + func finish(_ result: Result) + { + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let group): + // Assign App Group + // TODO: Determine whether app already belongs to app group. + + ALTAppleAPI.shared.add(appID, to: group, team: team) { (success, error) in + let result = result.map { _ in appID } + completionHandler(result) + } + } + } + + let adjustedGroupIdentifier = "group.\(team.identifier)." + groupIdentifier + + ALTAppleAPI.shared.fetchAppGroups(for: team) { (groups, error) in + switch Result(groups, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let groups): + + if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) + { + finish(.success(group)) + } + else + { + // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). + let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") + + ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team) { (group, error) in + finish(Result(group, error)) + } + } + } + } + } + func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result) -> Void) { ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in @@ -225,7 +386,7 @@ private extension ResignAppOperation } } - func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result) -> Void) -> Progress + func prepareAppBundle(for installedApp: InstalledApp, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 1) @@ -235,6 +396,22 @@ private extension ResignAppOperation let fileURL = installedApp.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 + } + + try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL) + } + DispatchQueue.global().async { do { @@ -244,9 +421,8 @@ private extension ResignAppOperation // Become current so we can observe progress from unzipAppBundle(). progress.becomeCurrent(withPendingUnitCount: 1) - guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) } - - guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) } + 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]] ?? [] @@ -255,15 +431,25 @@ private extension ResignAppOperation "CFBundleURLSchemes": [openURL.scheme!]] as [String : Any] allURLSchemes.append(altstoreURLScheme) - infoDictionary[Bundle.Info.urlTypes] = allURLSchemes - + var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes] + if appIdentifier == App.altstoreAppID { guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } - infoDictionary[Bundle.Info.deviceID] = udid + additionalValues[Bundle.Info.deviceID] = udid } - try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL) + // 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)) } @@ -276,9 +462,10 @@ private extension ResignAppOperation return progress } - func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result) -> Void) -> Progress + func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress { - let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in + + let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in do { try Result(success, error).get() diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 17cc07d3..8da82a59 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 17cc07d3b324aa1891bb32ebe5ffc836d3094dc5 +Subproject commit 8da82a59a2be1e83b6969a73c652b66110c350f5