From e855a1fb62f3e7304d69b80d6b8e4bea25919755 Mon Sep 17 00:00:00 2001 From: Kino Roy Date: Sat, 28 Sep 2024 16:25:11 -0700 Subject: [PATCH 1/5] Implement security key auth --- Xcodes.xcodeproj/project.pbxproj | 24 +++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++ Xcodes/AppleAPI/Sources/AppleAPI/Client.swift | 52 +++++++++++++-- .../Sources/AppleAPI/URLRequest+Apple.swift | 14 +++++ Xcodes/Backend/AppState.swift | 62 ++++++++++++++++++ Xcodes/Frontend/Common/XcodesSheet.swift | 6 +- Xcodes/Frontend/MainWindow.swift | 5 ++ Xcodes/Frontend/SignIn/SignIn2FAView.swift | 6 +- .../Frontend/SignIn/SignInPhoneListView.swift | 2 +- Xcodes/Frontend/SignIn/SignInSMSView.swift | 6 +- .../SignIn/SignInSecurityKeyPinView.swift | 63 +++++++++++++++++++ .../SignIn/SignInSecurityKeyTouchView.swift | 54 ++++++++++++++++ Xcodes/Resources/Licenses.rtf | 31 ++++++++- Xcodes/Resources/Localizable.xcstrings | 25 ++++++++ 14 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift create mode 100644 Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 7f86080c..1d0aeb9a 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; }; + 3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; }; + 332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; }; 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; }; 36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; }; 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; }; @@ -192,6 +195,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = ""; }; + 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = ""; }; 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = ""; }; 36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = ""; }; 536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = ""; }; @@ -346,6 +351,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */, CABFA9E42592F08E00380FEE /* Version in Frameworks */, CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */, E689540325BE8C64000EBCEA /* DockProgress in Frameworks */, @@ -454,6 +460,8 @@ CA735108257BF96D00EA9CF8 /* AttributedText.swift */, CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */, CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */, + 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */, + 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */, CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */, CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */, CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */, @@ -714,6 +722,7 @@ E8F44A1D296B4CD7002D6592 /* Path */, E84E4F562B335094003F3959 /* OrderedCollections */, E891A1C32B43ACF900A1B9D1 /* Sparkle */, + 334A932B2CA885A400A5E079 /* LibFido2Swift */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -802,6 +811,7 @@ E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */, E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */, E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */, + 33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */, ); productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; @@ -889,6 +899,7 @@ CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */, + 3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */, B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */, CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */, B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */, @@ -915,6 +926,7 @@ B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */, B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */, 53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */, + 332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */, CA61A6E0259835580008926E /* Xcode.swift in Sources */, CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */, CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, @@ -1469,6 +1481,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kinoroy/LibFido2Swift.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.1.0; + }; + }; CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xcodereleases/data"; @@ -1568,6 +1588,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 334A932B2CA885A400A5E079 /* LibFido2Swift */ = { + isa = XCSwiftPackageProductDependency; + productName = LibFido2Swift; + }; CA9FF86C25951C6E00E47BAF /* XCModel */ = { isa = XCSwiftPackageProductDependency; package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1dbb8fe2..14767fba 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -64,6 +64,15 @@ "version": "1.0.4" } }, + { + "package": "LibFido2Swift", + "repositoryURL": "https://github.com/kinoroy/LibFido2Swift.git", + "state": { + "branch": null, + "revision": "fc558a5e86adb8d404758d83fbc01fd02d99f1b1", + "version": "0.1.1" + } + }, { "package": "Path.swift", "repositoryURL": "https://github.com/mxcl/Path.swift", diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index 5b9bd081..dfc4cd0a 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -118,7 +118,7 @@ public class Client { case .twoStep: return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication) .eraseToAnyPublisher() - case .twoFactor: + case .twoFactor, .securityKey: return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions) .eraseToAnyPublisher() case .unknown: @@ -139,7 +139,10 @@ public class Client { // SMS wasn't sent automatically because user needs to choose a phone to send to } else if authOptions.canFallBackToSMS { option = .smsPendingChoice - // Code is shown on trusted devices + // Code is shown on trusted devices + } else if authOptions.fsaChallenge != nil { + option = .securityKey + // User needs to use a physical security key to respond to the challenge } else { option = .codeSent } @@ -193,6 +196,33 @@ public class Client { .eraseToAnyPublisher() } + public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher { + Result { + URLRequest.resposndToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response) + } + .publisher + .flatMap { request in + Current.network.dataTask(with: request) + .mapError { $0 as Error } + .tryMap { (data, response) throws -> (Data, URLResponse) in + guard let urlResponse = response as? HTTPURLResponse else { return (data, response) } + switch urlResponse.statusCode { + case 200..<300: + return (data, urlResponse) + case 400, 401: + throw AuthenticationError.incorrectSecurityCode + case 412: + throw AuthenticationError.appleIDAndPrivacyAcknowledgementRequired + case let code: + throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse) + } + } + .flatMap { (data, response) -> AnyPublisher in + self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) + } + }.eraseToAnyPublisher() + } + // MARK: - Session /// Use the olympus session endpoint to see if the existing session is still valid @@ -326,27 +356,37 @@ public enum TwoFactorOption: Equatable { case smsSent(AuthOptionsResponse.TrustedPhoneNumber) case codeSent case smsPendingChoice + case securityKey +} + +public struct FSAChallenge: Equatable, Decodable { + public let challenge: String + public let keyHandles: [String] + public let allowedCredentials: String } public struct AuthOptionsResponse: Equatable, Decodable { public let trustedPhoneNumbers: [TrustedPhoneNumber]? public let trustedDevices: [TrustedDevice]? - public let securityCode: SecurityCodeInfo + public let securityCode: SecurityCodeInfo? public let noTrustedDevices: Bool? public let serviceErrors: [ServiceError]? + public let fsaChallenge: FSAChallenge? public init( trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?, trustedDevices: [AuthOptionsResponse.TrustedDevice]?, securityCode: AuthOptionsResponse.SecurityCodeInfo, noTrustedDevices: Bool? = nil, - serviceErrors: [ServiceError]? = nil + serviceErrors: [ServiceError]? = nil, + fsaChallenge: FSAChallenge? = nil ) { self.trustedPhoneNumbers = trustedPhoneNumbers self.trustedDevices = trustedDevices self.securityCode = securityCode self.noTrustedDevices = noTrustedDevices self.serviceErrors = serviceErrors + self.fsaChallenge = fsaChallenge } public var kind: Kind { @@ -354,6 +394,8 @@ public struct AuthOptionsResponse: Equatable, Decodable { return .twoStep } else if trustedPhoneNumbers != nil { return .twoFactor + } else if fsaChallenge != nil { + return .securityKey } else { return .unknown } @@ -416,7 +458,7 @@ public struct AuthOptionsResponse: Equatable, Decodable { } public enum Kind: Equatable { - case twoStep, twoFactor, unknown + case twoStep, twoFactor, securityKey, unknown } } diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift index d052d637..db1dc43f 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift @@ -9,6 +9,7 @@ public extension URL { static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")! static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")! static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")! + static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")! } public extension URLRequest { @@ -105,6 +106,19 @@ public extension URLRequest { } return request } + + static func resposndToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest { + var request = URLRequest(url: .keyAuth) + request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID + request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey + request.allHTTPHeaderFields?["scnt"] = scnt + request.allHTTPHeaderFields?["Accept"] = "application/json" + request.allHTTPHeaderFields?["Content-Type"] = "application/json" + request.httpMethod = "POST" + request.httpBody = response + return request + } static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest { var request = URLRequest(url: .trust) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 46a5d633..48d41a55 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -9,6 +9,7 @@ import Version import os.log import DockProgress import XcodesKit +import LibFido2Swift class AppState: ObservableObject { private let client = AppleAPI.Client() @@ -320,6 +321,67 @@ class AppState: ObservableObject { .store(in: &cancellables) } + var fido2: FIDO2? + + func createAndSubmitSecurityKeyAssertationWithPinCode(_ pinCode: String, sessionData: AppleSessionData, authOptions: AuthOptionsResponse) { + self.presentedSheet = .securityKeyTouchToConfirm + + guard let fsaChallenge = authOptions.fsaChallenge else { + // This shouldn't happen + // we shouldn't have called this method without setting the fsaChallenge + // so this is an assertionFailure + assertionFailure() + self.authError = "Something went wrong. Please file a bug report" + return + } + + // The challenge is encoded in Base64URL encoding + let challengeUrl = fsaChallenge.challenge + let challenge = FIDO2.base64urlToBase64(base64url: challengeUrl) + let origin = "https://idmsa.apple.com" + let rpId = "apple.com" + // Allowed creds is sent as a comma separated string + let validCreds = fsaChallenge.allowedCredentials.split(separator: ",").map(String.init) + + Task { + do { + let fido2 = FIDO2() + self.fido2 = fido2 + let response = try fido2.respondToChallenge(args: ChallengeArgs(rpId: rpId, validCredentials: validCreds, devPin: pinCode, challenge: challenge, origin: origin)) + + Task { @MainActor in + self.isProcessingAuthRequest = true + } + + let respData = try JSONEncoder().encode(response) + client.submitChallenge(response: respData, sessionData: AppleSessionData(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)) + .receive(on: DispatchQueue.main) + .handleEvents( + receiveOutput: { authenticationState in + self.authenticationState = authenticationState + }, + receiveCompletion: { completion in + self.handleAuthenticationFlowCompletion(completion) + self.isProcessingAuthRequest = false + } + ).sink( + receiveCompletion: { _ in }, + receiveValue: { _ in } + ).store(in: &cancellables) + } catch FIDO2Error.canceledByUser { + // User cancelled the auth flow + // we don't have to show an error + // because the sheet will already be dismissed + } catch { + authError = error + } + } + } + + func cancelSecurityKeyAssertationRequest() { + self.fido2?.cancel() + } + private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion) { switch completion { case let .failure(error): diff --git a/Xcodes/Frontend/Common/XcodesSheet.swift b/Xcodes/Frontend/Common/XcodesSheet.swift index a0270b5c..2aa8f2c8 100644 --- a/Xcodes/Frontend/Common/XcodesSheet.swift +++ b/Xcodes/Frontend/Common/XcodesSheet.swift @@ -4,6 +4,7 @@ import AppleAPI enum XcodesSheet: Identifiable { case signIn case twoFactor(SecondFactorData) + case securityKeyTouchToConfirm var id: Int { Kind(self).hashValue } @@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable { extension XcodesSheet { private enum Kind: Hashable { - case signIn, twoFactor(TwoFactorOption) + case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm enum TwoFactorOption { case smsSent case codeSent case smsPendingChoice + case securityKeyPin } init(_ sheet: XcodesSheet) { @@ -32,7 +34,9 @@ extension XcodesSheet { case .smsSent: self = .twoFactor(.smsSent) case .smsPendingChoice: self = .twoFactor(.smsPendingChoice) case .codeSent: self = .twoFactor(.codeSent) + case .securityKey: self = .twoFactor(.securityKeyPin) } + case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm } } } diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index e2e972ab..698ba965 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -76,6 +76,9 @@ struct MainWindow: View { case .twoFactor(let secondFactorData): secondFactorView(secondFactorData) .environmentObject(appState) + case .securityKeyTouchToConfirm: + SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil) + .environmentObject(appState) } } .alert(item: $appState.presentedAlert, content: { presentedAlert in @@ -107,6 +110,8 @@ struct MainWindow: View { SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) case .smsPendingChoice: SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + case .securityKey: + SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) } } diff --git a/Xcodes/Frontend/SignIn/SignIn2FAView.swift b/Xcodes/Frontend/SignIn/SignIn2FAView.swift index 10950331..a8dfa988 100644 --- a/Xcodes/Frontend/SignIn/SignIn2FAView.swift +++ b/Xcodes/Frontend/SignIn/SignIn2FAView.swift @@ -10,12 +10,12 @@ struct SignIn2FAView: View { var body: some View { VStack(alignment: .leading) { - Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length)) + Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length)) .fixedSize(horizontal: true, vertical: false) HStack { Spacer() - PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) { + PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) { appState.submitSecurityCode(.device(code: $0), sessionData: sessionData) } Spacer() @@ -32,7 +32,7 @@ struct SignIn2FAView: View { Text("Continue") } .keyboardShortcut(.defaultAction) - .disabled(code.count != authOptions.securityCode.length) + .disabled(code.count != authOptions.securityCode!.length) } .frame(height: 25) } diff --git a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift index 70184e18..fc31d97b 100644 --- a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift +++ b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift @@ -11,7 +11,7 @@ struct SignInPhoneListView: View { var body: some View { VStack(alignment: .leading) { if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty { - Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length)) + Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode!.length)) List(phoneNumbers, selection: $selectedPhoneNumberID) { Text($0.numberWithDialCode) diff --git a/Xcodes/Frontend/SignIn/SignInSMSView.swift b/Xcodes/Frontend/SignIn/SignInSMSView.swift index c8a04dba..6d42f692 100644 --- a/Xcodes/Frontend/SignIn/SignInSMSView.swift +++ b/Xcodes/Frontend/SignIn/SignInSMSView.swift @@ -11,11 +11,11 @@ struct SignInSMSView: View { var body: some View { VStack(alignment: .leading) { - Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode)) + Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode)) HStack { Spacer() - PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) { + PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) { appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData) } Spacer() @@ -31,7 +31,7 @@ struct SignInSMSView: View { Text("Continue") } .keyboardShortcut(.defaultAction) - .disabled(code.count != authOptions.securityCode.length) + .disabled(code.count != authOptions.securityCode!.length) } .frame(height: 25) } diff --git a/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift b/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift new file mode 100644 index 00000000..32b6028c --- /dev/null +++ b/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift @@ -0,0 +1,63 @@ +// +// SignInSecurityKeyPin.swift +// Xcodes +// +// Created by Kino on 2024-09-26. +// Copyright © 2024 Robots and Pencils. All rights reserved. +// + +import SwiftUI +import AppleAPI + +struct SignInSecurityKeyPinView: View { + @EnvironmentObject var appState: AppState + @Binding var isPresented: Bool + @State private var pin: String = "" + let authOptions: AuthOptionsResponse + let sessionData: AppleSessionData + + var body: some View { + VStack(alignment: .leading) { + Text(localizeString("SecurityKeyPinDescription")) + .fixedSize(horizontal: true, vertical: false) + + HStack { + Spacer() + SecureField("PIN", text: $pin) + Spacer() + } + .padding() + + HStack { + Button("Cancel", action: { isPresented = false }) + .keyboardShortcut(.cancelAction) + Spacer() + ProgressButton(isInProgress: appState.isProcessingAuthRequest, + action: submitPinCode) { + Text("Continue") + } + .keyboardShortcut(.defaultAction) + // FIDO2 device pin codes must be at least 4 code points + // https://docs.yubico.com/yesdk/users-manual/application-fido2/fido2-pin.html + .disabled(pin.count < 4) + } + .frame(height: 25) + } + .padding() + .emittingError($appState.authError, recoveryHandler: { _ in }) + } + + func submitPinCode() { + appState.createAndSubmitSecurityKeyAssertationWithPinCode(pin, sessionData: sessionData, authOptions: authOptions) + } +} + +#Preview { + SignInSecurityKeyPinView(isPresented: .constant(true), + authOptions: AuthOptionsResponse( + trustedPhoneNumbers: nil, + trustedDevices: nil, + securityCode: .init(length: 6) + ), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")) + .environmentObject(AppState()) +} diff --git a/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift b/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift new file mode 100644 index 00000000..362a54fe --- /dev/null +++ b/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift @@ -0,0 +1,54 @@ +// +// SignInSecurityKeyPin.swift +// Xcodes +// +// Created by Kino on 2024-09-26. +// Copyright © 2024 Robots and Pencils. All rights reserved. +// + +import SwiftUI +import AppleAPI + +struct SignInSecurityKeyTouchView: View { + @EnvironmentObject var appState: AppState + @Binding var isPresented: Bool + + var body: some View { + VStack(alignment: .center) { + Image(systemName: "key.radiowaves.forward") + .font(.system(size: 32)).bold() + .padding(.bottom) + HStack { + Spacer() + Text(localizeString("SecurityKeyTouchDescription")) + .fixedSize(horizontal: true, vertical: false) + Spacer() + } + HStack { + Button("Cancel", action: self.cancel) + .keyboardShortcut(.cancelAction) + Spacer() + + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(x: 0.5, y: 0.5, anchor: .center) + .isHidden(!appState.isProcessingAuthRequest) + + .keyboardShortcut(.defaultAction) + } + .frame(height: 25) + } + .padding() + .emittingError($appState.authError, recoveryHandler: { _ in }) + } + + func cancel() { + appState.cancelSecurityKeyAssertationRequest() + isPresented = false + } +} + +#Preview { + SignInSecurityKeyTouchView(isPresented: .constant(true)) + .environmentObject(AppState()) +} diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 079a1375..2b586eac 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2759 +{\rtf1\ansi\ansicpg1252\cocoartf2818 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} @@ -58,6 +58,33 @@ SOFTWARE.\ \ \ +\fs34 LibFido2Swift\ +\ + +\fs26 MIT License\ +\ +Copyright (c) 2024 Kino Roy\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy\ +of this software and associated documentation files (the "Software"), to deal\ +in the Software without restriction, including without limitation the rights\ +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ +copies of the Software, and to permit persons to whom the Software is\ +furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all\ +copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ +SOFTWARE.\ +\ +\ + \fs34 ErrorHandling\ \ @@ -557,7 +584,7 @@ For more information, please refer to <>\ \fs26 MIT License\ \ -Copyright (c) Sindre Sorhus (sindresorhus.com)\ +Copyright (c) Sindre Sorhus (https://sindresorhus.com)\ \ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ \ diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 6d61d71d..524a88eb 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -17409,6 +17409,9 @@ } } } + }, + "PIN" : { + }, "Platforms" : { "localizations" : { @@ -19366,6 +19369,28 @@ } } }, + "SecurityKeyPinDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + } + } + }, + "SecurityKeyTouchDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + } + } + }, "Select" : { "localizations" : { "ar" : { From 604358bf6ca3e1ce00425985ec791496d9f5f5a1 Mon Sep 17 00:00:00 2001 From: Kino Roy Date: Sat, 12 Oct 2024 14:06:48 -0700 Subject: [PATCH 2/5] Update LibFido2Swift with Xcode 15 support --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14767fba..884e347e 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/kinoroy/LibFido2Swift.git", "state": { "branch": null, - "revision": "fc558a5e86adb8d404758d83fbc01fd02d99f1b1", - "version": "0.1.1" + "revision": "b77e5c6451bea69d15615d6578936b11777d9a6c", + "version": "0.1.2" } }, { From f269dcbc9837473a086331b965748c522993f606 Mon Sep 17 00:00:00 2001 From: Kino Roy Date: Sat, 12 Oct 2024 14:12:50 -0700 Subject: [PATCH 3/5] Fix typo --- Xcodes/AppleAPI/Sources/AppleAPI/Client.swift | 2 +- Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index dfc4cd0a..4a4cd62f 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -198,7 +198,7 @@ public class Client { public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher { Result { - URLRequest.resposndToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response) + URLRequest.respondToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response) } .publisher .flatMap { request in diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift index db1dc43f..cc30f3f2 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift @@ -107,7 +107,7 @@ public extension URLRequest { return request } - static func resposndToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest { + static func respondToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest { var request = URLRequest(url: .keyAuth) request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID From c3e568e23491987d57b1a483bd489ec425c3d152 Mon Sep 17 00:00:00 2001 From: Kino Roy Date: Sat, 12 Oct 2024 14:13:02 -0700 Subject: [PATCH 4/5] Add missing English localization --- Xcodes/Resources/Localizable.xcstrings | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 524a88eb..f53086cb 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -17411,7 +17411,14 @@ } }, "PIN" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + } + } }, "Platforms" : { "localizations" : { From 27bbf6f2a35e7254b032944b86e17e4b6d26c429 Mon Sep 17 00:00:00 2001 From: Kino Roy Date: Sat, 12 Oct 2024 20:26:19 -0700 Subject: [PATCH 5/5] Add missing localizations --- Xcodes/Resources/Localizable.xcstrings | 342 +++++++++++++++++++++++++ 1 file changed, 342 insertions(+) diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index f53086cb..de92c833 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -17412,11 +17412,125 @@ }, "PIN" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "PIN" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN" + } } } }, @@ -19379,22 +19493,250 @@ "SecurityKeyPinDescription" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Insert your physical security key and enter the PIN" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insert your physical security key and enter the PIN" + } } } }, "SecurityKeyTouchDescription" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Touch your security key to verify that it’s you" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch your security key to verify that it’s you" + } } } },