Implement security key auth

This commit is contained in:
Kino Roy 2024-09-28 16:25:11 -07:00
parent c94c2c1979
commit e855a1fb62
14 changed files with 344 additions and 15 deletions

View file

@ -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 = "<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>"; };
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>"; };
@ -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" */;

View file

@ -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",

View file

@ -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<AuthenticationState, Error> {
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<AuthenticationState, Error> 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
}
}

View file

@ -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)

View file

@ -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<Error>) {
switch completion {
case let .failure(error):

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}

View 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())
}

View 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())
}

View file

@ -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 &lt;<http://unlicense.org/>&gt;\
\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:\
\

View file

@ -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 its you"
}
}
}
},
"Select" : {
"localizations" : {
"ar" : {