mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge pull request #617 from kinoroy/security-key-auth
Implement Security Key Auth
This commit is contained in:
commit
8277554048
14 changed files with 693 additions and 15 deletions
|
|
@ -7,6 +7,9 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
|
||||||
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
||||||
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
||||||
|
|
@ -192,6 +195,8 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
|
||||||
|
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
|
||||||
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
|
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
|
||||||
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
|
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
|
||||||
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
|
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -346,6 +351,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
|
||||||
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
||||||
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
||||||
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
|
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
|
||||||
|
|
@ -454,6 +460,8 @@
|
||||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
||||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
||||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
||||||
|
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
|
||||||
|
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
|
||||||
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
||||||
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
||||||
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
||||||
|
|
@ -714,6 +722,7 @@
|
||||||
E8F44A1D296B4CD7002D6592 /* Path */,
|
E8F44A1D296B4CD7002D6592 /* Path */,
|
||||||
E84E4F562B335094003F3959 /* OrderedCollections */,
|
E84E4F562B335094003F3959 /* OrderedCollections */,
|
||||||
E83FDC432CBB649100679C6B /* Sparkle */,
|
E83FDC432CBB649100679C6B /* Sparkle */,
|
||||||
|
334A932B2CA885A400A5E079 /* LibFido2Swift */,
|
||||||
);
|
);
|
||||||
productName = XcodesMac;
|
productName = XcodesMac;
|
||||||
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
||||||
|
|
@ -802,6 +811,7 @@
|
||||||
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
|
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
|
||||||
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||||
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
|
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
|
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
|
||||||
);
|
);
|
||||||
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|
@ -889,6 +899,7 @@
|
||||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||||
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
||||||
|
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
|
||||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||||
|
|
@ -915,6 +926,7 @@
|
||||||
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
||||||
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
||||||
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
||||||
|
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
|
||||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||||
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
||||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
||||||
|
|
@ -1469,6 +1481,14 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference 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" */ = {
|
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/xcodereleases/data";
|
repositoryURL = "https://github.com/xcodereleases/data";
|
||||||
|
|
@ -1568,6 +1588,10 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = LibFido2Swift;
|
||||||
|
};
|
||||||
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
|
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
|
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,15 @@
|
||||||
"version": "1.0.4"
|
"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",
|
"package": "Path.swift",
|
||||||
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ public class Client {
|
||||||
case .twoStep:
|
case .twoStep:
|
||||||
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
case .twoFactor:
|
case .twoFactor, .securityKey:
|
||||||
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
|
@ -139,7 +139,10 @@ public class Client {
|
||||||
// SMS wasn't sent automatically because user needs to choose a phone to send to
|
// SMS wasn't sent automatically because user needs to choose a phone to send to
|
||||||
} else if authOptions.canFallBackToSMS {
|
} else if authOptions.canFallBackToSMS {
|
||||||
option = .smsPendingChoice
|
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 {
|
} else {
|
||||||
option = .codeSent
|
option = .codeSent
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +196,33 @@ public class Client {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
|
||||||
|
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<AuthenticationState, Error> in
|
||||||
|
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
|
||||||
|
}
|
||||||
|
}.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Session
|
// MARK: - Session
|
||||||
|
|
||||||
/// Use the olympus session endpoint to see if the existing session is still valid
|
/// 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 smsSent(AuthOptionsResponse.TrustedPhoneNumber)
|
||||||
case codeSent
|
case codeSent
|
||||||
case smsPendingChoice
|
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 struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
||||||
public let trustedDevices: [TrustedDevice]?
|
public let trustedDevices: [TrustedDevice]?
|
||||||
public let securityCode: SecurityCodeInfo
|
public let securityCode: SecurityCodeInfo?
|
||||||
public let noTrustedDevices: Bool?
|
public let noTrustedDevices: Bool?
|
||||||
public let serviceErrors: [ServiceError]?
|
public let serviceErrors: [ServiceError]?
|
||||||
|
public let fsaChallenge: FSAChallenge?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
|
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
|
||||||
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
|
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
|
||||||
securityCode: AuthOptionsResponse.SecurityCodeInfo,
|
securityCode: AuthOptionsResponse.SecurityCodeInfo,
|
||||||
noTrustedDevices: Bool? = nil,
|
noTrustedDevices: Bool? = nil,
|
||||||
serviceErrors: [ServiceError]? = nil
|
serviceErrors: [ServiceError]? = nil,
|
||||||
|
fsaChallenge: FSAChallenge? = nil
|
||||||
) {
|
) {
|
||||||
self.trustedPhoneNumbers = trustedPhoneNumbers
|
self.trustedPhoneNumbers = trustedPhoneNumbers
|
||||||
self.trustedDevices = trustedDevices
|
self.trustedDevices = trustedDevices
|
||||||
self.securityCode = securityCode
|
self.securityCode = securityCode
|
||||||
self.noTrustedDevices = noTrustedDevices
|
self.noTrustedDevices = noTrustedDevices
|
||||||
self.serviceErrors = serviceErrors
|
self.serviceErrors = serviceErrors
|
||||||
|
self.fsaChallenge = fsaChallenge
|
||||||
}
|
}
|
||||||
|
|
||||||
public var kind: Kind {
|
public var kind: Kind {
|
||||||
|
|
@ -354,6 +394,8 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
return .twoStep
|
return .twoStep
|
||||||
} else if trustedPhoneNumbers != nil {
|
} else if trustedPhoneNumbers != nil {
|
||||||
return .twoFactor
|
return .twoFactor
|
||||||
|
} else if fsaChallenge != nil {
|
||||||
|
return .securityKey
|
||||||
} else {
|
} else {
|
||||||
return .unknown
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +458,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Kind: Equatable {
|
public enum Kind: Equatable {
|
||||||
case twoStep, twoFactor, unknown
|
case twoStep, twoFactor, securityKey, unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ public extension URL {
|
||||||
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
|
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 federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
|
||||||
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
|
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 {
|
public extension URLRequest {
|
||||||
|
|
@ -105,6 +106,19 @@ public extension URLRequest {
|
||||||
}
|
}
|
||||||
return request
|
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 {
|
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
|
||||||
var request = URLRequest(url: .trust)
|
var request = URLRequest(url: .trust)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Version
|
||||||
import os.log
|
import os.log
|
||||||
import DockProgress
|
import DockProgress
|
||||||
import XcodesKit
|
import XcodesKit
|
||||||
|
import LibFido2Swift
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
private let client = AppleAPI.Client()
|
private let client = AppleAPI.Client()
|
||||||
|
|
@ -320,6 +321,67 @@ class AppState: ObservableObject {
|
||||||
.store(in: &cancellables)
|
.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<Error>) {
|
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||||
switch completion {
|
switch completion {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import AppleAPI
|
||||||
enum XcodesSheet: Identifiable {
|
enum XcodesSheet: Identifiable {
|
||||||
case signIn
|
case signIn
|
||||||
case twoFactor(SecondFactorData)
|
case twoFactor(SecondFactorData)
|
||||||
|
case securityKeyTouchToConfirm
|
||||||
|
|
||||||
var id: Int { Kind(self).hashValue }
|
var id: Int { Kind(self).hashValue }
|
||||||
|
|
||||||
|
|
@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
|
||||||
|
|
||||||
extension XcodesSheet {
|
extension XcodesSheet {
|
||||||
private enum Kind: Hashable {
|
private enum Kind: Hashable {
|
||||||
case signIn, twoFactor(TwoFactorOption)
|
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
|
||||||
|
|
||||||
enum TwoFactorOption {
|
enum TwoFactorOption {
|
||||||
case smsSent
|
case smsSent
|
||||||
case codeSent
|
case codeSent
|
||||||
case smsPendingChoice
|
case smsPendingChoice
|
||||||
|
case securityKeyPin
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ sheet: XcodesSheet) {
|
init(_ sheet: XcodesSheet) {
|
||||||
|
|
@ -32,7 +34,9 @@ extension XcodesSheet {
|
||||||
case .smsSent: self = .twoFactor(.smsSent)
|
case .smsSent: self = .twoFactor(.smsSent)
|
||||||
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
|
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
|
||||||
case .codeSent: self = .twoFactor(.codeSent)
|
case .codeSent: self = .twoFactor(.codeSent)
|
||||||
|
case .securityKey: self = .twoFactor(.securityKeyPin)
|
||||||
}
|
}
|
||||||
|
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ struct MainWindow: View {
|
||||||
case .twoFactor(let secondFactorData):
|
case .twoFactor(let secondFactorData):
|
||||||
secondFactorView(secondFactorData)
|
secondFactorView(secondFactorData)
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
|
case .securityKeyTouchToConfirm:
|
||||||
|
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
|
||||||
|
.environmentObject(appState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $appState.presentedAlert, content: { presentedAlert in
|
.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)
|
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
case .smsPendingChoice:
|
case .smsPendingChoice:
|
||||||
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
|
case .securityKey:
|
||||||
|
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ struct SignIn2FAView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
|
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length))
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
|
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
|
||||||
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
|
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -32,7 +32,7 @@ struct SignIn2FAView: View {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
.disabled(code.count != authOptions.securityCode.length)
|
.disabled(code.count != authOptions.securityCode!.length)
|
||||||
}
|
}
|
||||||
.frame(height: 25)
|
.frame(height: 25)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct SignInPhoneListView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
|
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) {
|
List(phoneNumbers, selection: $selectedPhoneNumberID) {
|
||||||
Text($0.numberWithDialCode)
|
Text($0.numberWithDialCode)
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ struct SignInSMSView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
|
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode))
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
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)
|
appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -31,7 +31,7 @@ struct SignInSMSView: View {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
.disabled(code.count != authOptions.securityCode.length)
|
.disabled(code.count != authOptions.securityCode!.length)
|
||||||
}
|
}
|
||||||
.frame(height: 25)
|
.frame(height: 25)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift
Normal file
63
Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
54
Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift
Normal file
54
Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{\rtf1\ansi\ansicpg1252\cocoartf2759
|
{\rtf1\ansi\ansicpg1252\cocoartf2818
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
||||||
{\colortbl;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;}
|
||||||
{\*\expandedcolortbl;;}
|
{\*\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\
|
\fs34 ErrorHandling\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
|
@ -557,7 +584,7 @@ For more information, please refer to <<http://unlicense.org/>>\
|
||||||
|
|
||||||
\fs26 MIT License\
|
\fs26 MIT License\
|
||||||
\
|
\
|
||||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\
|
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (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:\
|
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:\
|
||||||
\
|
\
|
||||||
|
|
|
||||||
|
|
@ -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" : {
|
"Platforms" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"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" : {
|
"Select" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue