Compare commits

...

2 Commits

3 changed files with 167 additions and 62 deletions

View File

@@ -85,70 +85,86 @@ final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, A
return return
} }
// Sign In Task {
self.signIn() { (result) in // try to use cached session
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } if
let certificate = Keychain.shared.certificate,
switch result let session = Keychain.shared.session,
let team = Keychain.shared.team
{ {
case .failure(let error): self.finish(.failure(error)) if session.anisetteData.date.timeIntervalSinceNow < -40.0 {
case .success((let account, let session)): let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
self.context.session = session let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
self.progress.completedUnitCount += 1 fetchAnisetteDataOperation.resultHandler = { (result) in
c.resume(with: result)
// Fetch Team
self.fetchTeam(for: account, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let team):
self.context.team = team
self.progress.completedUnitCount += 1
// Fetch Certificate
self.fetchCertificate(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let certificate):
self.context.certificate = certificate
self.progress.completedUnitCount += 1
// Register Device
self.registerCurrentDevice(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
// Save account/team to disk.
self.save(team) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(team: team, session: session) { (result) in
let result = result.map { _ in (team, certificate, session) }
self.finish(result)
}
}
}
}
}
}
} }
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
session.anisetteData = anisetteData
}
self.context.team = team
self.context.session = session
self.context.certificate = certificate
self.finish(.success((team, certificate, session)))
return
}
// new login
do {
let (account, session) = try await withUnsafeThrowingContinuation { c in
self.signIn() { (result) in
c.resume(with: result)
} }
} }
self.context.session = session
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let team = try await withUnsafeThrowingContinuation { c in
self.fetchTeam(for: account, session: session) { (result) in
c.resume(with: result)
}
}
self.context.team = team
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let certificate = try await withUnsafeThrowingContinuation { c in
self.fetchCertificate(for: team, session: session) { (result) in
c.resume(with: result)
}
}
self.context.certificate = certificate
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
let _ = try await withUnsafeThrowingContinuation { c in
self.registerCurrentDevice(for: team, session: session) { (result) in
c.resume(with: result)
}
}
self.progress.completedUnitCount += 1
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
try await withUnsafeThrowingContinuation { c in
self.save(team) { (result) in
c.resume(with: result)
}
}
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
try await withUnsafeThrowingContinuation { c in
self.cacheAppIDs(team: team, session: session) { (result) in
c.resume(with: result)
}
}
Keychain.shared.team = team
Keychain.shared.certificate = certificate
Keychain.shared.session = session
self.finish(.success((team, certificate, session)))
} catch {
self.finish(.failure(error))
} }
} }
} }
@@ -359,6 +375,29 @@ private extension AuthenticationOperation
} }
} }
if let adsid = Keychain.shared.appleIDAdsid, let xcodeToken = Keychain.shared.appleIDXcodeToken {
Logger.sideload.notice("Authenticating Apple ID with tokens...")
let semaphore = DispatchSemaphore(value: 0)
var shouldContinue = true
Task {
defer {
semaphore.signal()
}
do {
let (account, session) = try await self.authenticateWithToken(adsid: adsid, xcodeToken: xcodeToken)
completionHandler(.success((account, session)))
shouldContinue = false
} catch {
Logger.sideload.notice("Authentication failed with token. Fall back to email and password login: \(error)")
}
}
semaphore.wait()
if !shouldContinue {
return
}
}
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{ {
Logger.sideload.notice("Authenticating Apple ID...") Logger.sideload.notice("Authenticating Apple ID...")
@@ -384,6 +423,25 @@ private extension AuthenticationOperation
} }
} }
func authenticateWithToken(adsid: String, xcodeToken: String) async throws -> (ALTAccount, ALTAppleAPISession) {
let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in
c.resume(with: result)
}
self.operationQueue.addOperation(fetchAnisetteDataOperation)
}
let session = ALTAppleAPISession(dsid: adsid, authToken: xcodeToken, anisetteData: anisetteData)
let account = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAccount, any Error>) in
ALTAppleAPI.shared.fetchAccount2(session: session) { result in
c.resume(with: result)
}
}
return (account, session)
}
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void) func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{ {
self.appleIDEmailAddress = appleID self.appleIDEmailAddress = appleID
@@ -444,6 +502,8 @@ private extension AuthenticationOperation
verificationHandler: verificationHandler) { (account, session, error) in verificationHandler: verificationHandler) { (account, session, error) in
if let account = account, let session = session if let account = account, let session = session
{ {
Keychain.shared.appleIDAdsid = session.dsid
Keychain.shared.appleIDXcodeToken = session.authToken
completionHandler(.success((account, session))) completionHandler(.success((account, session)))
} }
else else
@@ -747,3 +807,30 @@ extension AuthenticationOperation
self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6 self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
} }
} }
extension ALTAppleAPI {
func fetchAccount2(session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
{
let url = URL(string: "viewDeveloper.action", relativeTo: self.baseURL)!
self.sendRequest(with: url, additionalParameters: nil, session: session, team: nil) { (responseDictionary, requestError) in
do
{
guard let responseDictionary = responseDictionary else { throw requestError ?? ALTAppleAPIError.unknown() }
guard let account = try self.processResponse(responseDictionary, parseHandler: { () -> Any? in
guard let dictionary = responseDictionary["developer"] as? [String: Any] else { return nil }
let account = ALTAccount(responseDictionary: dictionary)
return account
}, resultCodeHandler: nil) as? ALTAccount else {
throw ALTAppleAPIError.unknown()
}
completionHandler(.success(account))
} catch {
completionHandler(.failure(error))
}
}
}
}

View File

@@ -198,7 +198,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX") formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian) formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone.current formatter.timeZone = TimeZone.init(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let dateString = formatter.string(from: Date()) let dateString = formatter.string(from: Date())
formattedJSON["date"] = dateString formattedJSON["date"] = dateString

View File

@@ -53,6 +53,12 @@ public class Keychain
@KeychainItem(key: "appleIDPassword") @KeychainItem(key: "appleIDPassword")
public var appleIDPassword: String? public var appleIDPassword: String?
@KeychainItem(key: "appleIDAdsid")
public var appleIDAdsid: String?
@KeychainItem(key: "appleIDXcodeToken")
public var appleIDXcodeToken: String?
@KeychainItem(key: "signingCertificatePrivateKey") @KeychainItem(key: "signingCertificatePrivateKey")
public var signingCertificatePrivateKey: Data? public var signingCertificatePrivateKey: Data?
@@ -83,6 +89,12 @@ public class Keychain
@KeychainItem(key: "adiPb") @KeychainItem(key: "adiPb")
public var adiPb: String? public var adiPb: String?
// for some reason authenticated cert/session/team is completely not cached, which result in logging in for every request
// we save it here so when user logs out we can clear cached account/session/team
public var certificate: ALTCertificate? = nil
public var session: ALTAppleAPISession? = nil
public var team: ALTTeam? = nil
private init() private init()
{ {
} }
@@ -91,7 +103,13 @@ public class Keychain
{ {
self.appleIDEmailAddress = nil self.appleIDEmailAddress = nil
self.appleIDPassword = nil self.appleIDPassword = nil
self.appleIDAdsid = nil
self.appleIDXcodeToken = nil
self.signingCertificatePrivateKey = nil self.signingCertificatePrivateKey = nil
self.signingCertificateSerialNumber = nil self.signingCertificateSerialNumber = nil
self.certificate = nil
self.session = nil
self.team = nil
} }
} }