[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

@@ -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
}
}