Skip to content

Commit 8277554

Browse files
authored
Merge pull request #617 from kinoroy/security-key-auth
Implement Security Key Auth
2 parents ba11d41 + f4567bd commit 8277554

File tree

14 files changed

+693
-15
lines changed

14 files changed

+693
-15
lines changed

Xcodes.xcodeproj/project.pbxproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
11+
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
12+
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
1013
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
1114
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
1215
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
@@ -192,6 +195,8 @@
192195
/* End PBXCopyFilesBuildPhase section */
193196

194197
/* Begin PBXFileReference section */
198+
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
199+
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
195200
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
196201
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
197202
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
@@ -346,6 +351,7 @@
346351
isa = PBXFrameworksBuildPhase;
347352
buildActionMask = 2147483647;
348353
files = (
354+
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
349355
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
350356
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
351357
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
@@ -454,6 +460,8 @@
454460
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
455461
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
456462
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
463+
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
464+
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
457465
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
458466
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
459467
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
@@ -714,6 +722,7 @@
714722
E8F44A1D296B4CD7002D6592 /* Path */,
715723
E84E4F562B335094003F3959 /* OrderedCollections */,
716724
E83FDC432CBB649100679C6B /* Sparkle */,
725+
334A932B2CA885A400A5E079 /* LibFido2Swift */,
717726
);
718727
productName = XcodesMac;
719728
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@@ -802,6 +811,7 @@
802811
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
803812
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
804813
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
814+
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
805815
);
806816
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
807817
projectDirPath = "";
@@ -889,6 +899,7 @@
889899
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
890900
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
891901
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
902+
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
892903
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
893904
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
894905
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
@@ -915,6 +926,7 @@
915926
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
916927
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
917928
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
929+
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
918930
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
919931
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
920932
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
@@ -1469,6 +1481,14 @@
14691481
/* End XCConfigurationList section */
14701482

14711483
/* Begin XCRemoteSwiftPackageReference section */
1484+
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
1485+
isa = XCRemoteSwiftPackageReference;
1486+
repositoryURL = "https://github.com/kinoroy/LibFido2Swift.git";
1487+
requirement = {
1488+
kind = upToNextMinorVersion;
1489+
minimumVersion = 0.1.0;
1490+
};
1491+
};
14721492
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
14731493
isa = XCRemoteSwiftPackageReference;
14741494
repositoryURL = "https://github.com/xcodereleases/data";
@@ -1568,6 +1588,10 @@
15681588
/* End XCRemoteSwiftPackageReference section */
15691589

15701590
/* Begin XCSwiftPackageProductDependency section */
1591+
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
1592+
isa = XCSwiftPackageProductDependency;
1593+
productName = LibFido2Swift;
1594+
};
15711595
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
15721596
isa = XCSwiftPackageProductDependency;
15731597
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;

Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Xcodes/AppleAPI/Sources/AppleAPI/Client.swift

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public class Client {
118118
case .twoStep:
119119
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
120120
.eraseToAnyPublisher()
121-
case .twoFactor:
121+
case .twoFactor, .securityKey:
122122
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
123123
.eraseToAnyPublisher()
124124
case .unknown:
@@ -139,7 +139,10 @@ public class Client {
139139
// SMS wasn't sent automatically because user needs to choose a phone to send to
140140
} else if authOptions.canFallBackToSMS {
141141
option = .smsPendingChoice
142-
// Code is shown on trusted devices
142+
// Code is shown on trusted devices
143+
} else if authOptions.fsaChallenge != nil {
144+
option = .securityKey
145+
// User needs to use a physical security key to respond to the challenge
143146
} else {
144147
option = .codeSent
145148
}
@@ -193,6 +196,33 @@ public class Client {
193196
.eraseToAnyPublisher()
194197
}
195198

199+
public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
200+
Result {
201+
URLRequest.respondToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response)
202+
}
203+
.publisher
204+
.flatMap { request in
205+
Current.network.dataTask(with: request)
206+
.mapError { $0 as Error }
207+
.tryMap { (data, response) throws -> (Data, URLResponse) in
208+
guard let urlResponse = response as? HTTPURLResponse else { return (data, response) }
209+
switch urlResponse.statusCode {
210+
case 200..<300:
211+
return (data, urlResponse)
212+
case 400, 401:
213+
throw AuthenticationError.incorrectSecurityCode
214+
case 412:
215+
throw AuthenticationError.appleIDAndPrivacyAcknowledgementRequired
216+
case let code:
217+
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
218+
}
219+
}
220+
.flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
221+
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
222+
}
223+
}.eraseToAnyPublisher()
224+
}
225+
196226
// MARK: - Session
197227

198228
/// Use the olympus session endpoint to see if the existing session is still valid
@@ -326,34 +356,46 @@ public enum TwoFactorOption: Equatable {
326356
case smsSent(AuthOptionsResponse.TrustedPhoneNumber)
327357
case codeSent
328358
case smsPendingChoice
359+
case securityKey
360+
}
361+
362+
public struct FSAChallenge: Equatable, Decodable {
363+
public let challenge: String
364+
public let keyHandles: [String]
365+
public let allowedCredentials: String
329366
}
330367

331368
public struct AuthOptionsResponse: Equatable, Decodable {
332369
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
333370
public let trustedDevices: [TrustedDevice]?
334-
public let securityCode: SecurityCodeInfo
371+
public let securityCode: SecurityCodeInfo?
335372
public let noTrustedDevices: Bool?
336373
public let serviceErrors: [ServiceError]?
374+
public let fsaChallenge: FSAChallenge?
337375

338376
public init(
339377
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
340378
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
341379
securityCode: AuthOptionsResponse.SecurityCodeInfo,
342380
noTrustedDevices: Bool? = nil,
343-
serviceErrors: [ServiceError]? = nil
381+
serviceErrors: [ServiceError]? = nil,
382+
fsaChallenge: FSAChallenge? = nil
344383
) {
345384
self.trustedPhoneNumbers = trustedPhoneNumbers
346385
self.trustedDevices = trustedDevices
347386
self.securityCode = securityCode
348387
self.noTrustedDevices = noTrustedDevices
349388
self.serviceErrors = serviceErrors
389+
self.fsaChallenge = fsaChallenge
350390
}
351391

352392
public var kind: Kind {
353393
if trustedDevices != nil {
354394
return .twoStep
355395
} else if trustedPhoneNumbers != nil {
356396
return .twoFactor
397+
} else if fsaChallenge != nil {
398+
return .securityKey
357399
} else {
358400
return .unknown
359401
}
@@ -416,7 +458,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
416458
}
417459

418460
public enum Kind: Equatable {
419-
case twoStep, twoFactor, unknown
461+
case twoStep, twoFactor, securityKey, unknown
420462
}
421463
}
422464

Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public extension URL {
99
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
1010
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
1111
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
12+
static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")!
1213
}
1314

1415
public extension URLRequest {
@@ -105,6 +106,19 @@ public extension URLRequest {
105106
}
106107
return request
107108
}
109+
110+
static func respondToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest {
111+
var request = URLRequest(url: .keyAuth)
112+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
113+
request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID
114+
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
115+
request.allHTTPHeaderFields?["scnt"] = scnt
116+
request.allHTTPHeaderFields?["Accept"] = "application/json"
117+
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
118+
request.httpMethod = "POST"
119+
request.httpBody = response
120+
return request
121+
}
108122

109123
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
110124
var request = URLRequest(url: .trust)

Xcodes/Backend/AppState.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Version
99
import os.log
1010
import DockProgress
1111
import XcodesKit
12+
import LibFido2Swift
1213

1314
class AppState: ObservableObject {
1415
private let client = AppleAPI.Client()
@@ -320,6 +321,67 @@ class AppState: ObservableObject {
320321
.store(in: &cancellables)
321322
}
322323

324+
var fido2: FIDO2?
325+
326+
func createAndSubmitSecurityKeyAssertationWithPinCode(_ pinCode: String, sessionData: AppleSessionData, authOptions: AuthOptionsResponse) {
327+
self.presentedSheet = .securityKeyTouchToConfirm
328+
329+
guard let fsaChallenge = authOptions.fsaChallenge else {
330+
// This shouldn't happen
331+
// we shouldn't have called this method without setting the fsaChallenge
332+
// so this is an assertionFailure
333+
assertionFailure()
334+
self.authError = "Something went wrong. Please file a bug report"
335+
return
336+
}
337+
338+
// The challenge is encoded in Base64URL encoding
339+
let challengeUrl = fsaChallenge.challenge
340+
let challenge = FIDO2.base64urlToBase64(base64url: challengeUrl)
341+
let origin = "https://idmsa.apple.com"
342+
let rpId = "apple.com"
343+
// Allowed creds is sent as a comma separated string
344+
let validCreds = fsaChallenge.allowedCredentials.split(separator: ",").map(String.init)
345+
346+
Task {
347+
do {
348+
let fido2 = FIDO2()
349+
self.fido2 = fido2
350+
let response = try fido2.respondToChallenge(args: ChallengeArgs(rpId: rpId, validCredentials: validCreds, devPin: pinCode, challenge: challenge, origin: origin))
351+
352+
Task { @MainActor in
353+
self.isProcessingAuthRequest = true
354+
}
355+
356+
let respData = try JSONEncoder().encode(response)
357+
client.submitChallenge(response: respData, sessionData: AppleSessionData(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt))
358+
.receive(on: DispatchQueue.main)
359+
.handleEvents(
360+
receiveOutput: { authenticationState in
361+
self.authenticationState = authenticationState
362+
},
363+
receiveCompletion: { completion in
364+
self.handleAuthenticationFlowCompletion(completion)
365+
self.isProcessingAuthRequest = false
366+
}
367+
).sink(
368+
receiveCompletion: { _ in },
369+
receiveValue: { _ in }
370+
).store(in: &cancellables)
371+
} catch FIDO2Error.canceledByUser {
372+
// User cancelled the auth flow
373+
// we don't have to show an error
374+
// because the sheet will already be dismissed
375+
} catch {
376+
authError = error
377+
}
378+
}
379+
}
380+
381+
func cancelSecurityKeyAssertationRequest() {
382+
self.fido2?.cancel()
383+
}
384+
323385
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
324386
switch completion {
325387
case let .failure(error):

Xcodes/Frontend/Common/XcodesSheet.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AppleAPI
44
enum XcodesSheet: Identifiable {
55
case signIn
66
case twoFactor(SecondFactorData)
7+
case securityKeyTouchToConfirm
78

89
var id: Int { Kind(self).hashValue }
910

@@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
1617

1718
extension XcodesSheet {
1819
private enum Kind: Hashable {
19-
case signIn, twoFactor(TwoFactorOption)
20+
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
2021

2122
enum TwoFactorOption {
2223
case smsSent
2324
case codeSent
2425
case smsPendingChoice
26+
case securityKeyPin
2527
}
2628

2729
init(_ sheet: XcodesSheet) {
@@ -32,7 +34,9 @@ extension XcodesSheet {
3234
case .smsSent: self = .twoFactor(.smsSent)
3335
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
3436
case .codeSent: self = .twoFactor(.codeSent)
37+
case .securityKey: self = .twoFactor(.securityKeyPin)
3538
}
39+
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
3640
}
3741
}
3842
}

Xcodes/Frontend/MainWindow.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ struct MainWindow: View {
7676
case .twoFactor(let secondFactorData):
7777
secondFactorView(secondFactorData)
7878
.environmentObject(appState)
79+
case .securityKeyTouchToConfirm:
80+
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
81+
.environmentObject(appState)
7982
}
8083
}
8184
.alert(item: $appState.presentedAlert, content: { presentedAlert in
@@ -107,6 +110,8 @@ struct MainWindow: View {
107110
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
108111
case .smsPendingChoice:
109112
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
113+
case .securityKey:
114+
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
110115
}
111116
}
112117

0 commit comments

Comments
 (0)