[AltStore] Revises authentication flow with better UI

This commit is contained in:
Riley Testut
2019-06-05 18:05:21 -07:00
parent 5c4613fd20
commit 0895e4238f
15 changed files with 1323 additions and 258 deletions

View File

@@ -144,6 +144,13 @@
BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; };
BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; };
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; };
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; };
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFFC044E22A204F40066B31F /* App.ipa in Resources */ = {isa = PBXBuildFile; fileRef = BFFC044D22A204F30066B31F /* App.ipa */; };
DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; };
/* End PBXBuildFile section */
@@ -354,6 +361,13 @@
BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; };
BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; };
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceCertificateViewController.swift; sourceTree = "<group>"; };
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
BFFC044D22A204F30066B31F /* App.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = App.ipa; sourceTree = "<group>"; };
EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -657,6 +671,7 @@
children = (
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
BFD247732284B9A500981D42 /* Main.storyboard */,
BFE6325822A83BA800F30809 /* Authentication */,
BFD2478A2284C49000981D42 /* Apps */,
BFBBE2E2229320A2002097FA /* My Apps */,
BFB1169E22933DDC00BB457C /* Updates */,
@@ -730,8 +745,10 @@
children = (
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */,
BFB11691229322E400BB457C /* DatabaseManager.swift */,
BFE6326722A858F300F30809 /* Account.swift */,
BFBBE2DE22931F73002097FA /* App.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFE6326522A857C100F30809 /* Team.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -754,6 +771,18 @@
path = Connections;
sourceTree = "<group>";
};
BFE6325822A83BA800F30809 /* Authentication */ = {
isa = PBXGroup;
children = (
BFE6325922A83BEB00F30809 /* Authentication.storyboard */,
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */,
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */,
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
);
path = Authentication;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -956,6 +985,7 @@
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1096,23 +1126,29 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */,
BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */,
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */,
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
);

View File

@@ -39,6 +39,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Keychain.shared.appleIDEmailAddress = nil
Keychain.shared.appleIDPassword = nil
Keychain.shared.signingCertificatePrivateKey = nil
Keychain.shared.signingCertificateIdentifier = nil
UserDefaults.standard.firstLaunch = Date()
}

View File

@@ -86,16 +86,19 @@ private extension AppDetailViewController
self.descriptionLabel.text = self.app.localizedDescription
if self.app.installedApp == nil
if !self.downloadButton.isIndicatingActivity
{
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
self.downloadButton.setTitle(text, for: .normal)
self.downloadButton.isEnabled = true
}
else
{
self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
self.downloadButton.isEnabled = false
if self.app.installedApp == nil
{
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
self.downloadButton.setTitle(text, for: .normal)
self.downloadButton.isEnabled = true
}
else
{
self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
self.downloadButton.isEnabled = false
}
}
}
@@ -135,6 +138,10 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2)
}
}
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled)
{
// Ignore
}
catch
{
DispatchQueue.main.async {
@@ -145,8 +152,8 @@ private extension AppDetailViewController
}
DispatchQueue.main.async {
self.update()
sender.isIndicatingActivity = false
self.update()
}
}
}

View File

@@ -60,9 +60,11 @@ class AppManager
static let shared = AppManager()
private let session = URLSession(configuration: .default)
private let operationQueue = OperationQueue()
private init()
{
self.operationQueue.name = "com.rileytestut.AltStore.AppManager"
}
}
@@ -97,6 +99,15 @@ extension AppManager
print("Error while fetching installed apps")
}
}
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void)
{
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
completionHandler(result)
}
self.operationQueue.addOperation(authenticationOperation)
}
}
extension AppManager
@@ -131,14 +142,14 @@ extension AppManager
switch result
{
case .failure(let error): finish(.failure(.authentication(error)))
case .success(let team):
case .success(let team, let certificate):
// Fetch signing resources
self.fetchSigningResources(for: app, team: team, presentingViewController: presentingViewController) { (result) in
// Fetch provisioning profile
self.prepareProvisioningProfile(for: app, team: team) { (result) in
switch result
{
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
case .success(let certificate, let profile):
case .success(let profile):
// Prepare app
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
@@ -233,44 +244,37 @@ extension AppManager
switch result
{
case .failure(let error): finish(.failure(.authentication(error)))
case .success(let team):
case .success(let team, let certificate):
// Fetch Certificate
self.fetchCertificate(for: team, presentingViewController: nil) { (result) in
switch result
{
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
case .success(let certificate):
let signer = ALTSigner(team: team, certificate: certificate)
// Sign
let signer = ALTSigner(team: team, certificate: certificate)
let dispatchGroup = DispatchGroup()
var results = [String: Result<InstalledApp, Error>]()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
for app in installedApps
{
dispatchGroup.enter()
app.managedObjectContext?.perform {
let bundleIdentifier = app.bundleIdentifier
print("Refreshing App:", bundleIdentifier)
let dispatchGroup = DispatchGroup()
var results = [String: Result<InstalledApp, Error>]()
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
for app in installedApps
{
dispatchGroup.enter()
app.managedObjectContext?.perform {
let bundleIdentifier = app.bundleIdentifier
print("Refreshing App:", bundleIdentifier)
self.refresh(app, signer: signer, context: context) { (result) in
print("Refreshed App: \(bundleIdentifier).", result)
results[bundleIdentifier] = result
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global()) {
context.perform {
finish(.success(results))
}
self.refresh(app, signer: signer, context: context) { (result) in
print("Refreshed App: \(bundleIdentifier).", result)
results[bundleIdentifier] = result
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global()) {
context.perform {
finish(.success(results))
}
}
}
}
}
@@ -299,63 +303,6 @@ private extension AppManager
downloadTask.resume()
}
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
{
func authenticate(emailAddress: String, password: String)
{
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
do
{
let account = try Result(account, error).get()
Keychain.shared.appleIDEmailAddress = emailAddress
Keychain.shared.appleIDPassword = password
self.fetchTeam(for: account, presentingViewController: presentingViewController, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(error))
}
}
}
if let emailAddress = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
authenticate(emailAddress: emailAddress, password: password)
}
else if let presentingViewController = presentingViewController
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Enter Apple ID + Password", message: "", preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.placeholder = "Apple ID"
textField.textContentType = .emailAddress
}
alertController.addTextField { (textField) in
textField.placeholder = "Password"
textField.textContentType = .password
}
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: "Sign In", style: .default) { [unowned alertController] (action) in
guard
let emailAddress = alertController.textFields![0].text,
let password = alertController.textFields![1].text,
!emailAddress.isEmpty, !password.isEmpty
else { return completionHandler(.failure(ALTAppleAPIError(.incorrectCredentials))) }
authenticate(emailAddress: emailAddress, password: password)
})
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
else
{
completionHandler(.failure(AppError.notAuthenticated))
}
}
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) }
@@ -397,32 +344,6 @@ private extension AppManager
}
}
func fetchSigningResources(for app: App, team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTCertificate, ALTProvisioningProfile), Error>) -> Void)
{
self.fetchCertificate(for: team, presentingViewController: presentingViewController) { (result) in
do
{
let certificate = try result.get()
self.prepareProvisioningProfile(for: app, team: team) { (result) in
do
{
let provisioningProfile = try result.get()
completionHandler(.success((certificate, provisioningProfile)))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
{
do
@@ -484,132 +405,6 @@ private extension AppManager
private extension AppManager
{
func fetchTeam(for account: ALTAccount, presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
{
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
do
{
let teams = try Result(teams, error).get()
guard teams.count > 0 else { throw ALTAppleAPIError(.noTeams) }
if let team = teams.first, teams.count == 1
{
completionHandler(.success(team))
}
else
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Select Team", message: "", preferredStyle: .actionSheet)
alertController.addAction(.cancel)
for team in teams
{
alertController.addAction(UIAlertAction(title: team.name, style: .default) { (action) in
completionHandler(.success(team))
})
}
presentingViewController?.present(alertController, animated: true, completion: nil)
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func fetchCertificate(for team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
if
let identifier = UserDefaults.standard.signingCertificateIdentifier,
let privateKey = Keychain.shared.signingCertificatePrivateKey,
let certificate = certificates.first(where: { $0.identifier == identifier })
{
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
else if certificates.count < 1
{
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw AppError.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
throw AppError.missingCertificate
}
certificate.privateKey = privateKey
UserDefaults.standard.signingCertificateIdentifier = certificate.identifier
Keychain.shared.signingCertificatePrivateKey = privateKey
completionHandler(.success(certificate))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
else if let presentingViewController = presentingViewController
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Too Many Certificates", message: "Please select the certificate you would like to revoke.", preferredStyle: .actionSheet)
alertController.addAction(.cancel)
for certificate in certificates
{
alertController.addAction(UIAlertAction(title: certificate.name, style: .default) { (action) in
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
do
{
try Result(success, error).get()
self.fetchCertificate(for: team, presentingViewController: presentingViewController, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(error))
}
}
})
}
presentingViewController.present(alertController, animated: true, completion: nil)
}
}
else
{
completionHandler(.failure(AppError.multipleCertificates))
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in

View File

@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Apple ID-->
<scene sceneID="3cc-cd-zDK">
<objects>
<tableViewController storyboardIdentifier="authenticationViewController" id="nRn-xt-2XS" customClass="AuthenticationViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" estimatedRowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="r38-H3-S3C">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<sections>
<tableViewSection id="uDm-cx-LdY">
<string key="footerTitle">Your email address and password are used only to sign in with Apple and is never stored.
If you have two-factor authentication enabled, make sure to use an app-specific password.</string>
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ER5-4r-tld">
<rect key="frame" x="0.0" y="35" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ER5-4r-tld" id="BnC-HI-d8z">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="70T-cn-6XF">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apple ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="09n-b4-DRC">
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="Y87-hZ-IsD"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email Address" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="V6B-NM-wpL">
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" returnKeyType="next" enablesReturnKeyAutomatically="YES" textContentType="email"/>
<connections>
<outlet property="delegate" destination="nRn-xt-2XS" id="5Us-OB-B4F"/>
</connections>
</textField>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="70T-cn-6XF" firstAttribute="top" secondItem="BnC-HI-d8z" secondAttribute="top" id="Zyt-OB-o6T"/>
<constraint firstAttribute="trailingMargin" secondItem="70T-cn-6XF" secondAttribute="trailing" id="lYn-uy-vRk"/>
<constraint firstAttribute="bottom" secondItem="70T-cn-6XF" secondAttribute="bottom" id="urj-EQ-5WK"/>
<constraint firstItem="70T-cn-6XF" firstAttribute="leading" secondItem="BnC-HI-d8z" secondAttribute="leadingMargin" id="yqr-Kr-I93"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="E9B-Cb-M5e">
<rect key="frame" x="0.0" y="79" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="E9B-Cb-M5e" id="S4n-4w-12m">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="pON-cO-VYR">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vqv-cC-kya">
<rect key="frame" x="0.0" y="0.0" width="74" height="43.5"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="Egk-ba-Kh3"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="z98-Sm-yDv">
<rect key="frame" x="90" y="0.0" width="253" height="43.5"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" returnKeyType="go" enablesReturnKeyAutomatically="YES" secureTextEntry="YES" textContentType="password"/>
<connections>
<outlet property="delegate" destination="nRn-xt-2XS" id="7pH-Sf-Wmb"/>
</connections>
</textField>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="pON-cO-VYR" secondAttribute="trailing" id="IPH-Og-2ch"/>
<constraint firstAttribute="bottom" secondItem="pON-cO-VYR" secondAttribute="bottom" id="j7H-Ds-pJg"/>
<constraint firstItem="pON-cO-VYR" firstAttribute="leading" secondItem="S4n-4w-12m" secondAttribute="leadingMargin" id="uAc-4j-0pB"/>
<constraint firstItem="pON-cO-VYR" firstAttribute="top" secondItem="S4n-4w-12m" secondAttribute="top" id="xZe-CS-STZ"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="nRn-xt-2XS" id="VWO-oe-ykv"/>
<outlet property="delegate" destination="nRn-xt-2XS" id="CL1-Go-uiO"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Apple ID" id="viw-66-ZJ7">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="KXh-qW-MIA">
<connections>
<action selector="cancel" destination="nRn-xt-2XS" id="l1X-bA-xsz"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Sign In" style="done" id="mkE-Q8-CxO">
<connections>
<action selector="authenticate" destination="nRn-xt-2XS" id="q60-9N-xVb"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="emailAddressTextField" destination="V6B-NM-wpL" id="N3F-eI-yhE"/>
<outlet property="passwordTextField" destination="z98-Sm-yDv" id="WDu-6c-oBa"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="v2u-D2-stc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="605.60000000000002" y="19.340329835082461"/>
</scene>
<!--Select Team-->
<scene sceneID="0Hb-4t-vQ3">
<objects>
<tableViewController storyboardIdentifier="selectTeamViewController" id="R11-Yh-Wb1" customClass="SelectTeamViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="g2d-7w-OVl">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="iCV-rW-IhB" detailTextLabel="2hi-el-KvN" style="IBUITableViewCellStyleSubtitle" id="pPa-pY-koy">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pPa-pY-koy" id="DjO-Wt-6j2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="iCV-rW-IhB">
<rect key="frame" x="16" y="5" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2hi-el-KvN">
<rect key="frame" x="16" y="25.5" width="33" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="R11-Yh-Wb1" id="zkX-xW-GvZ"/>
<outlet property="delegate" destination="R11-Yh-Wb1" id="vP7-NA-Y0n"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Select Team" id="ALr-U3-Ucl">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="HUE-P1-xa1">
<connections>
<action selector="cancel" destination="R11-Yh-Wb1" id="Ckg-bQ-0nv"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="7Ou-hQ-Cr3">
<connections>
<action selector="chooseTeam:" destination="R11-Yh-Wb1" id="nin-nM-lxU"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HxT-dJ-1Ry" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1354" y="20"/>
</scene>
<!--Replace Certificate-->
<scene sceneID="fW2-QW-a2Z">
<objects>
<tableViewController storyboardIdentifier="replaceCertificateViewController" id="LAG-dk-a0f" customClass="ReplaceCertificateViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="enT-LI-CNI">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="luH-7x-QoO" style="IBUITableViewCellStyleDefault" id="i0O-XG-rRJ">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i0O-XG-rRJ" id="GCT-3I-GCy">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="luH-7x-QoO">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="LAG-dk-a0f" id="kOS-KX-Duz"/>
<outlet property="delegate" destination="LAG-dk-a0f" id="plW-kJ-BmR"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Replace Certificate" id="BM2-Vg-AJk">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="lPC-Dj-3Ik">
<connections>
<action selector="cancel" destination="LAG-dk-a0f" id="5C2-Hg-Les"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Next" style="done" id="ndJ-l9-HeM">
<connections>
<action selector="replaceCertificate:" destination="LAG-dk-a0f" id="vl2-E6-qi4"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yxU-EG-3sE" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2135" y="19"/>
</scene>
</scenes>
<color key="tintColor" name="Purple"/>
</document>

View File

@@ -0,0 +1,382 @@
//
// AuthenticationOperation.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
extension AuthenticationOperation
{
enum Error: LocalizedError
{
case cancelled
case notAuthenticated
case noTeam
case noCertificate
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self {
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
}
}
}
class AuthenticationOperation: RSTOperation
{
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
private weak var presentingViewController: UIViewController?
private lazy var navigationController = UINavigationController()
private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
private var appleIDPassword: String?
override var isAsynchronous: Bool {
return true
}
init(presentingViewController: UIViewController?)
{
self.presentingViewController = presentingViewController
super.init()
}
override func main()
{
super.main()
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
{
print("Finished authenticating with result:", result)
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let (altTeam, altCertificate) = try result.get()
let altAccount = altTeam.account
// Account
let account = Account(altAccount, context: context)
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
otherAccounts.forEach(context.delete(_:))
// Team
let team = Team(altTeam, account: account, context: context)
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
let otherTeams = try context.fetch(otherTeamsFetchRequest)
otherTeams.forEach(context.delete(_:))
// Save
try context.save()
// Update keychain
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
self.resultHandler?(.success((altTeam, altCertificate)))
}
catch
{
self.resultHandler?(.failure(error))
}
self.finish()
DispatchQueue.main.async {
self.navigationController.dismiss(animated: true, completion: nil)
}
RSTEndBackgroundTask(backgroundTaskID)
}
}
// Sign In
self.signIn { (result) in
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let account):
// Fetch Team
self.fetchTeam(for: account) { (result) in
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let team):
// Fetch Certificate
self.fetchCertificate(for: team) { (result) in
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let certificate): finish(.success((team, certificate)))
}
}
}
}
}
}
}
}
private extension AuthenticationOperation
{
func present(_ viewController: UIViewController) -> Bool
{
guard let presentingViewController = self.presentingViewController else { return false }
self.navigationController.view.tintColor = .altPurple
if self.navigationController.viewControllers.isEmpty
{
self.navigationController.setViewControllers([viewController], animated: false)
presentingViewController.present(self.navigationController, animated: true, completion: nil)
}
else
{
viewController.navigationItem.leftBarButtonItem = nil
self.navigationController.pushViewController(viewController, animated: true)
}
return true
}
}
private extension AuthenticationOperation
{
func signIn(completionHandler: @escaping (Result<ALTAccount, Swift.Error>) -> Void)
{
func authenticate()
{
DispatchQueue.main.async {
let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
authenticationViewController.authenticationHandler = { (result) in
if let (account, password) = result
{
self.appleIDPassword = password
completionHandler(.success(account))
}
else
{
completionHandler(.failure(Error.cancelled))
}
}
if !self.present(authenticationViewController)
{
completionHandler(.failure(Error.notAuthenticated))
}
}
}
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in
do
{
self.appleIDPassword = password
let account = try Result(account, error).get()
completionHandler(.success(account))
}
catch ALTAppleAPIError.incorrectCredentials
{
authenticate()
}
catch ALTAppleAPIError.appSpecificPasswordRequired
{
authenticate()
}
catch
{
completionHandler(.failure(error))
}
}
}
else
{
authenticate()
}
}
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
{
func selectTeam(from teams: [ALTTeam])
{
DispatchQueue.main.async {
let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController
selectTeamViewController.teams = teams
selectTeamViewController.selectionHandler = { (team) in
if let team = team
{
completionHandler(.success(team))
}
else
{
completionHandler(.failure(Error.cancelled))
}
}
if !self.present(selectTeamViewController)
{
completionHandler(.failure(Error.noTeam))
}
}
}
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
switch Result(teams, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let teams):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let fetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
fetchRequest.fetchLimit = 1
fetchRequest.returnsObjectsAsFaults = false
let fetchedTeams = try context.fetch(fetchRequest)
if let fetchedTeam = fetchedTeams.first, let altTeam = teams.first(where: { $0.identifier == fetchedTeam.identifier })
{
completionHandler(.success(altTeam))
}
else
{
selectTeam(from: teams)
}
}
catch
{
print("Error fetching Teams.", error)
selectTeam(from: teams)
}
}
}
}
}
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
{
func requestCertificate()
{
let machineName = "AltStore - " + UIDevice.current.name
ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey }
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
throw Error.missingCertificate
}
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func replaceCertificate(from certificates: [ALTCertificate])
{
DispatchQueue.main.async {
let replaceCertificateViewController = self.storyboard.instantiateViewController(withIdentifier: "replaceCertificateViewController") as! ReplaceCertificateViewController
replaceCertificateViewController.team = team
replaceCertificateViewController.certificates = certificates
replaceCertificateViewController.replacementHandler = { (certificate) in
if certificate != nil
{
requestCertificate()
}
else
{
completionHandler(.failure(Error.cancelled))
}
}
if !self.present(replaceCertificateViewController)
{
completionHandler(.failure(Error.noCertificate))
}
}
}
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
if
let identifier = Keychain.shared.signingCertificateIdentifier,
let privateKey = Keychain.shared.signingCertificatePrivateKey,
let certificate = certificates.first(where: { $0.identifier == identifier })
{
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
else if certificates.isEmpty
{
requestCertificate()
}
else
{
replaceCertificate(from: certificates)
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}

View File

@@ -0,0 +1,151 @@
//
// AuthenticationViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
class AuthenticationViewController: UITableViewController
{
var authenticationHandler: (((ALTAccount, String)?) -> Void)?
private var _didLayoutSubviews = false
@IBOutlet private var emailAddressTextField: UITextField!
@IBOutlet private var passwordTextField: UITextField!
override func viewDidLoad()
{
super.viewDidLoad()
self.update()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if !_didLayoutSubviews
{
self.emailAddressTextField.becomeFirstResponder()
}
_didLayoutSubviews = true
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
}
}
private extension AuthenticationViewController
{
func update()
{
if let _ = self.validate()
{
self.navigationItem.rightBarButtonItem?.isEnabled = true
}
else
{
self.navigationItem.rightBarButtonItem?.isEnabled = false
}
}
func validate() -> (String, String)?
{
guard
let emailAddress = self.emailAddressTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty,
let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty
else { return nil }
return (emailAddress, password)
}
func authenticate(emailAddress: String, password: String, completionHandler: @escaping (Result<(ALTAccount, [ALTTeam]), Error>) -> Void)
{
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
switch Result(account, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let account):
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
let result = Result(teams, error).map { (account, $0) }
completionHandler(result)
}
}
}
}
}
private extension AuthenticationViewController
{
@IBAction func authenticate()
{
guard let (emailAddress, password) = self.validate() else { return }
self.emailAddressTextField.resignFirstResponder()
self.passwordTextField.resignFirstResponder()
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in
do
{
let account = try Result(account, error).get()
self.authenticationHandler?((account, password))
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
}
}
}
}
@IBAction func cancel()
{
self.authenticationHandler?(nil)
}
}
extension AuthenticationViewController: UITextFieldDelegate
{
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
switch textField
{
case self.emailAddressTextField: self.passwordTextField.becomeFirstResponder()
case self.passwordTextField: self.authenticate()
default: break
}
self.update()
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
DispatchQueue.main.async {
self.update()
}
return true
}
}

View File

@@ -0,0 +1,156 @@
//
// ReplaceCertificateViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
extension ReplaceCertificateViewController
{
private enum Error: LocalizedError
{
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
switch self
{
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
}
}
}
}
class ReplaceCertificateViewController: UITableViewController
{
var replacementHandler: ((ALTCertificate?) -> Void)?
var team: ALTTeam!
var certificates: [ALTCertificate] {
get {
return self.dataSource.items
}
set {
self.dataSource.items = newValue
}
}
private var selectedCertificate: ALTCertificate? {
didSet {
self.update()
}
}
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.update()
}
}
private extension ReplaceCertificateViewController
{
func makeDataSource() -> RSTArrayTableViewDataSource<ALTCertificate>
{
let dataSource = RSTArrayTableViewDataSource<ALTCertificate>(items: [])
dataSource.proxy = self
dataSource.cellConfigurationHandler = { [weak self] (cell, certificate, indexPath) in
cell.textLabel?.text = certificate.name
cell.accessoryType = (self?.selectedCertificate == certificate) ? .checkmark : .none
}
let placeholderView = RSTPlaceholderView(frame: .zero)
placeholderView.textLabel.text = NSLocalizedString("No Certificates", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("There are no certificates associated with this team.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
func update()
{
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedCertificate != nil)
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
}
private extension ReplaceCertificateViewController
{
@IBAction func replaceCertificate(_ sender: UIBarButtonItem)
{
guard let certificate = self.selectedCertificate else { return }
func replace()
{
sender.isIndicatingActivity = true
ALTAppleAPI.shared.revoke(certificate, for: self.team) { (success, error) in
let result = Result(success, error).map { certificate }
do
{
let certificate = try result.get()
self.replacementHandler?(certificate)
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Error Replacing Certificate", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
sender.isIndicatingActivity = false
}
}
}
}
let localizedTitle = String(format: NSLocalizedString("Are you sure you want to replace %@?", comment: ""), certificate.name)
let localizedMessage = NSLocalizedString("Any AltStore apps currently installed with this certificate will need to be refreshed.", comment: "")
let localizedReplaceActionTitle = String(format: NSLocalizedString("Replace %@", comment: ""), certificate.name)
let alertController = UIAlertController(title: localizedTitle, message: localizedMessage, preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: localizedReplaceActionTitle, style: .destructive) { (action) in
replace()
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
@IBAction func cancel()
{
self.replacementHandler?(nil)
}
}
extension ReplaceCertificateViewController
{
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
return NSLocalizedString("You have reached the maximum number of development certificates. Please select a certificate to replace.", comment: "")
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let certificate = self.dataSource.item(at: indexPath)
self.selectedCertificate = certificate
}
}

View File

@@ -0,0 +1,150 @@
//
// SelectTeamViewController.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import AltSign
import Roxas
class SelectTeamViewController: UITableViewController
{
var selectionHandler: ((ALTTeam?) -> Void)?
var teams: [ALTTeam] {
get {
return self.dataSource.items
}
set {
self.dataSource.items = newValue
}
}
private var selectedTeam: ALTTeam? {
didSet {
self.update()
}
}
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.update()
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
}
}
private extension SelectTeamViewController
{
func makeDataSource() -> RSTArrayTableViewDataSource<ALTTeam>
{
let dataSource = RSTArrayTableViewDataSource<ALTTeam>(items: [])
dataSource.proxy = self
dataSource.cellConfigurationHandler = { [weak self] (cell, team, indexPath) in
cell.textLabel?.text = team.name
switch team.type
{
case .unknown: cell.detailTextLabel?.text = NSLocalizedString("Unknown", comment: "")
case .free: cell.detailTextLabel?.text = NSLocalizedString("Free Developer Account", comment: "")
case .individual: cell.detailTextLabel?.text = NSLocalizedString("Individual", comment: "")
case .organization: cell.detailTextLabel?.text = NSLocalizedString("Organization", comment: "")
@unknown default: cell.detailTextLabel?.text = nil
}
cell.accessoryType = (self?.selectedTeam == team) ? .checkmark : .none
}
let placeholderView = RSTPlaceholderView(frame: .zero)
placeholderView.textLabel.text = NSLocalizedString("No Teams", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("You are not a member of any development teams.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
func update()
{
self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedTeam != nil)
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
func fetchCertificates(for team: ALTTeam, completionHandler: @escaping (Result<[ALTCertificate], Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificate, error) in
let result = Result(certificate, error)
completionHandler(result)
}
}
}
private extension SelectTeamViewController
{
@IBAction func chooseTeam(_ sender: UIBarButtonItem)
{
guard let team = self.selectedTeam else { return }
func choose()
{
sender.isIndicatingActivity = true
self.selectionHandler?(team)
}
if team.type == .organization
{
let localizedActionTitle = String(format: NSLocalizedString("Use %@?", comment: ""), team.name)
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to use an Organization team?", comment: ""),
message: NSLocalizedString("Doing so may affect other members of this team.", comment: ""), preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: localizedActionTitle, style: .destructive, handler: { (action) in
choose()
}))
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
else
{
choose()
}
}
@IBAction func cancel()
{
self.selectionHandler?(nil)
}
}
extension SelectTeamViewController
{
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
return NSLocalizedString("Select the team you would like to use to install apps.", comment: "")
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let team = self.dataSource.item(at: indexPath)
self.selectedTeam = team
}
}

View File

@@ -53,4 +53,14 @@ extension Keychain
self.keychain[data: "signingCertificatePrivateKey"] = newValue
}
}
var signingCertificateIdentifier: String? {
get {
let identifier = try? self.keychain.get("signingCertificateIdentifier")
return identifier
}
set {
self.keychain["signingCertificateIdentifier"] = newValue
}
}
}

View File

@@ -11,5 +11,4 @@ import Foundation
extension UserDefaults
{
@NSManaged var firstLaunch: Date?
@NSManaged var signingCertificateIdentifier: String?
}

View File

@@ -0,0 +1,59 @@
//
// Account.swift
// AltStore
//
// Created by Riley Testut on 6/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
@objc(Account)
class Account: NSManagedObject
{
var localizedName: String {
var components = PersonNameComponents()
components.givenName = self.firstName
components.familyName = self.lastName
let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default)
return name
}
/* Properties */
@NSManaged var appleID: String
@NSManaged var identifier: String
@NSManaged var firstName: String
@NSManaged var lastName: String
/* Relationships */
@NSManaged var teams: Set<Team>
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(_ account: ALTAccount, context: NSManagedObjectContext)
{
super.init(entity: Account.entity(), insertInto: context)
self.appleID = account.appleID
self.identifier = account.identifier
self.firstName = account.firstName
self.lastName = account.lastName
}
}
extension Account
{
@nonobjc class func fetchRequest() -> NSFetchRequest<Account>
{
return NSFetchRequest<Account>(entityName: "Account")
}
}

View File

@@ -1,5 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String" syncable="YES"/>
<attribute name="firstName" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="lastName" attributeType="String" syncable="YES"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="App" representedClassName="App" syncable="YES">
<attribute name="developerName" attributeType="String" syncable="YES"/>
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
@@ -25,8 +37,21 @@
<attribute name="version" attributeType="String" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="App" positionX="-63" positionY="-18" width="128" height="210"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="105"/>
<element name="Account" positionX="-36" positionY="90" width="128" height="120"/>
</elements>
</model>

52
AltStore/Model/Team.swift Normal file
View File

@@ -0,0 +1,52 @@
//
// Team.swift
// AltStore
//
// Created by Riley Testut on 5/31/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
@objc(Team)
class Team: NSManagedObject
{
/* Properties */
@NSManaged var name: String
@NSManaged var identifier: String
@NSManaged var type: ALTTeamType
/* Relationships */
@NSManaged private(set) var account: Account!
var altTeam: ALTTeam?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext)
{
super.init(entity: Team.entity(), insertInto: context)
self.altTeam = team
self.name = team.name
self.identifier = team.identifier
self.type = team.type
self.account = account
}
}
extension Team
{
@nonobjc class func fetchRequest() -> NSFetchRequest<Team>
{
return NSFetchRequest<Team>(entityName: "Team")
}
}