diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 2a117659..3f73c764 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 */, E83FDC432CBB649100679C6B /* Sparkle */, + 334A932B2CA885A400A5E079 /* LibFido2Swift */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -802,6 +811,7 @@ E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */, E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */, E83FDC422CBB649100679C6B /* 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 003e3c65..6acf19b7 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": "b77e5c6451bea69d15615d6578936b11777d9a6c", + "version": "0.1.2" + } + }, { "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..4a4cd62f 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.respondToChallenge(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..cc30f3f2 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 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 + 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 e8be0e61..11906294 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 52ea8280..b1cd3b96 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -17535,6 +17535,130 @@ } } }, + "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" + } + } + } + }, "Platforms" : { "localizations" : { "ar" : { @@ -19491,6 +19615,256 @@ } } }, + "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" + } + } + } + }, "Select" : { "localizations" : { "ar" : {