Resigns + installs test app to connected devices

This commit is contained in:
Riley Testut
2019-05-29 15:50:53 -07:00
parent 1ece81ca4a
commit f7beccbaa6
7 changed files with 552 additions and 23 deletions

View File

@@ -709,22 +709,131 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Lkz-Gl-wBp">
<rect key="frame" x="148" y="118" width="184" height="32"/>
<buttonCell key="cell" type="push" title="List Connected Devices" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MKD-1Q-nZx">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="listConnectedDevices:" target="XfG-lQ-9wD" id="zmE-CS-Q4o"/>
</connections>
</button>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bO3-CU-R3w">
<rect key="frame" x="90" y="49" width="300" height="172"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JQK-wm-ZlO">
<rect key="frame" x="0.0" y="103" width="300" height="69"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gSB-oz-v9o">
<rect key="frame" x="-2" y="52" width="56" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Apple ID" id="9BA-bC-wbL">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7XQ-t2-tot">
<rect key="frame" x="0.0" y="26" width="300" height="22"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Email Address" drawsBackground="YES" id="BcE-BW-rdX">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="RJ6-Gp-oBs">
<rect key="frame" x="0.0" y="0.0" width="300" height="22"/>
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="Oxo-HS-d2N">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<allowedInputSourceLocales>
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
</allowedInputSourceLocales>
</secureTextFieldCell>
</secureTextField>
</subviews>
<constraints>
<constraint firstItem="7XQ-t2-tot" firstAttribute="width" secondItem="JQK-wm-ZlO" secondAttribute="width" id="Cvc-T9-R6G"/>
<constraint firstAttribute="width" constant="300" id="KPr-Ft-Wmt"/>
<constraint firstItem="RJ6-Gp-oBs" firstAttribute="width" secondItem="JQK-wm-ZlO" secondAttribute="width" id="nIJ-OV-uhm"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BNl-oy-n0L">
<rect key="frame" x="0.0" y="41" width="300" height="42"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VdC-tO-mWt">
<rect key="frame" x="-2" y="25" width="46" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Device" id="kxw-TA-9uT">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fSQ-0R-QEy">
<rect key="frame" x="-2" y="-3" width="305" height="25"/>
<popUpButtonCell key="cell" type="push" title="Item 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tR5-jT-cUe" id="2ld-VC-nij">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="A1j-uZ-XuR">
<items>
<menuItem title="Item 1" state="on" id="tR5-jT-cUe"/>
<menuItem title="Item 2" id="8GQ-cV-M1u"/>
<menuItem title="Item 3" id="pw6-Y7-sH0"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="chooseDevice:" target="XfG-lQ-9wD" id="QWe-hT-gGV"/>
</connections>
</popUpButton>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="300" id="H6n-um-gRo"/>
<constraint firstItem="fSQ-0R-QEy" firstAttribute="width" secondItem="BNl-oy-n0L" secondAttribute="width" id="bqm-hA-4Dz"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Lkz-Gl-wBp">
<rect key="frame" x="86" y="-7" width="129" height="32"/>
<buttonCell key="cell" type="push" title="Install AltStore" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MKD-1Q-nZx">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="installAltStore:" target="XfG-lQ-9wD" id="8N9-fZ-aYR"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="Lkz-Gl-wBp" firstAttribute="centerY" secondItem="m2S-Jp-Qdl" secondAttribute="centerY" id="2ik-iR-0j4"/>
<constraint firstItem="Lkz-Gl-wBp" firstAttribute="centerX" secondItem="m2S-Jp-Qdl" secondAttribute="centerX" id="oPG-tb-YM0"/>
<constraint firstItem="bO3-CU-R3w" firstAttribute="centerY" secondItem="m2S-Jp-Qdl" secondAttribute="centerY" id="H0J-sc-0Mn"/>
<constraint firstItem="bO3-CU-R3w" firstAttribute="centerX" secondItem="m2S-Jp-Qdl" secondAttribute="centerX" id="bfU-fD-Ihv"/>
</constraints>
</view>
<connections>
<outlet property="devicesButton" destination="fSQ-0R-QEy" id="dNS-Ox-X2J"/>
<outlet property="emailAddressTextField" destination="7XQ-t2-tot" id="BYZ-a3-3Je"/>
<outlet property="passwordTextField" destination="RJ6-Gp-oBs" id="aae-TM-o3X"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>

View File

@@ -0,0 +1,41 @@
//
// Result+Conveniences.swift
// AltStore
//
// Created by Riley Testut on 5/22/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension Result
{
init(_ value: Success?, _ error: Failure?)
{
switch (value, error)
{
case (let value?, _): self = .success(value)
case (_, let error?): self = .failure(error)
case (nil, nil): preconditionFailure("Either value or error must be non-nil")
}
}
}
extension Result where Success == Void
{
init(_ success: Bool, _ error: Failure?)
{
if success
{
self = .success(())
}
else if let error = error
{
self = .failure(error)
}
else
{
preconditionFailure("Error must be non-nil if success is false")
}
}
}

View File

@@ -8,19 +8,358 @@
import Cocoa
class ViewController: NSViewController {
enum InstallError: Error
{
case invalidCredentials
case noTeam
case missingPrivateKey
case missingCertificate
var localizedDescription: String {
switch self
{
case .invalidCredentials: return "The provided Apple ID and password are incorrect."
case .noTeam: return "You are not a member of any developer teams."
case .missingPrivateKey: return "The developer certificate's private key could not be found."
case .missingCertificate: return "The developer certificate could not be found."
}
}
}
class ViewController: NSViewController
{
@IBOutlet private var emailAddressTextField: NSTextField!
@IBOutlet private var passwordTextField: NSSecureTextField!
@IBOutlet private var devicesButton: NSPopUpButton!
private var currentDevice: ALTDevice?
override func viewDidLoad()
{
super.viewDidLoad()
self.update()
}
func update()
{
self.devicesButton.removeAllItems()
let devices = ALTDeviceManager.shared.connectedDevices
if devices.isEmpty
{
self.devicesButton.addItem(withTitle: "No Connected Device")
}
else
{
for device in devices
{
self.devicesButton.addItem(withTitle: device.name)
}
}
if let currentDevice = self.currentDevice, let index = devices.firstIndex(of: currentDevice)
{
self.devicesButton.selectItem(at: index)
}
else
{
self.currentDevice = devices.first
self.devicesButton.selectItem(at: 0)
}
}
}
private extension ViewController
{
@IBAction func listConnectedDevices(_ sender: NSButton)
@IBAction func installAltStore(_ sender: NSButton)
{
guard let device = self.currentDevice else { return }
guard !self.emailAddressTextField.stringValue.isEmpty, !self.passwordTextField.stringValue.isEmpty else { return }
self.installAltStore(to: device)
}
@IBAction func chooseDevice(_ sender: NSPopUpButton)
{
let devices = ALTDeviceManager.shared.connectedDevices
print(devices)
guard !devices.isEmpty else { return }
let index = sender.indexOfSelectedItem
let device = devices[index]
self.currentDevice = device
}
}
private extension ViewController
{
func installAltStore(to device: ALTDevice)
{
func present(_ error: Error, title: String)
{
DispatchQueue.main.async {
let alert = NSAlert(error: error)
alert.runModal()
}
}
self.authenticate() { (result) in
do
{
let account = try result.get()
self.fetchTeam(for: account) { (result) in
do
{
let team = try result.get()
self.register(device, team: team) { (result) in
do
{
let device = try result.get()
self.fetchCertificate(for: team) { (result) in
do
{
let certificate = try result.get()
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
do
{
let provisioningProfile = try result.get()
try self.installIPA(to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile)
}
catch
{
present(error, title: "Failed to Fetch Provisioning Profile")
}
}
}
catch
{
present(error, title: "Failed to Register App")
}
}
}
catch
{
present(error, title: "Failed to Fetch Certificate")
}
}
}
catch
{
present(error, title: "Failed to Register Device")
}
}
}
catch
{
present(error, title: "Failed to Fetch Team")
}
}
}
catch
{
present(error, title: "Failed to Authenticate")
}
}
}
func authenticate(completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
{
ALTAppleAPI.shared.authenticate(appleID: self.emailAddressTextField.stringValue, password: self.passwordTextField.stringValue) { (account, error) in
let result = Result(account, error)
completionHandler(result)
}
}
func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
{
ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in
do
{
let teams = try Result(teams, error).get()
guard let team = teams.first else { throw InstallError.noTeam }
completionHandler(.success(team))
}
catch
{
completionHandler(.failure(error))
}
}
}
func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
if let certificate = certificates.first
{
ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in
do
{
try Result(success, error).get()
self.fetchCertificate(for: team, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(error))
}
}
}
else
{
ALTAppleAPI.shared.addCertificate(machineName: "AltStore", to: team) { (certificate, error) in
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw InstallError.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 InstallError.missingCertificate
}
certificate.privateKey = privateKey
completionHandler(.success(certificate))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func registerAppID(name appName: String, identifier: String, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
var bundleID = "com." + team.account.firstName.lowercased() + team.account.lastName.lowercased() + "." + identifier
bundleID = bundleID.replacingOccurrences(of: " ", with: "")
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
{
completionHandler(.success(appID))
}
else
{
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
completionHandler(Result(appID, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == device.identifier })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
completionHandler(Result(profile, error))
}
}
func installIPA(to device: ALTDevice, team: ALTTeam, appID: ALTAppID, certificate: ALTCertificate, profile: ALTProvisioningProfile) throws
{
let ipaURL = Bundle.main.url(forResource: "App", withExtension: ".ipa")!
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
do
{
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: destinationDirectoryURL)
print(appBundleURL)
let infoPlistURL = appBundleURL.appendingPathComponent("Info.plist")
guard var infoDictionary = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
infoDictionary["altstore"] = ["udid": device.identifier]
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
let zippedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
let resigner = ALTSigner(team: team, certificate: certificate)
resigner.signApp(at: zippedURL, provisioningProfile: profile) { (resignedURL, error) in
do
{
let resignedURL = try Result(resignedURL, error).get()
ALTDeviceManager.shared.installApp(at: resignedURL, to: device) { (success, error) in
let result = Result(success, error)
print(result)
}
}
catch
{
print("Failed to install app", error)
}
}
}
catch
{
print("Failed to install AltStore", error)
}
}
}