This commit is contained in:
Adam Velebil 2024-03-26 11:54:43 +01:00
commit 91397d264a
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
15 changed files with 445 additions and 22 deletions

View File

@ -334,7 +334,9 @@ class MainActivity : FlutterFragmentActivity() {
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"),
fidoViewModel.fingerprints.streamTo(this, messenger, "android.fido.fingerprints"),
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
fidoViewModel.registerFingerprint.streamTo(this, messenger, "android.fido.registerFp"),
)
viewModel.appContext.observe(this) {
@ -348,7 +350,10 @@ class MainActivity : FlutterFragmentActivity() {
// only recreate the contextManager object if it cannot be reused
if (appContext == OperationContext.Home ||
(appContext == OperationContext.Oath && contextManager is OathManager) ||
(appContext == OperationContext.FidoPasskeys && contextManager is FidoManager)
(appContext in listOf(
OperationContext.FidoPasskeys,
OperationContext.FidoFingerprints
) && contextManager is FidoManager)
) {
// no need to dispose this context
} else {
@ -367,6 +372,7 @@ class MainActivity : FlutterFragmentActivity() {
appPreferences
)
OperationContext.FidoFingerprints,
OperationContext.FidoPasskeys -> FidoManager(
messenger,
deviceManager,

View File

@ -23,7 +23,10 @@ enum class FidoActionDescription(private val value: Int) {
Unlock(1),
SetPin(2),
DeleteCredential(3),
ActionFailure(4);
DeleteFingerprint(4),
RenameFingerprint(5),
RegisterFingerprint(6),
ActionFailure(7);
val id: Int
get() = value + dialogDescriptionFidoIndex

View File

@ -16,16 +16,17 @@
package com.yubico.authenticator.fido
import android.nfc.TagLostException
import com.yubico.authenticator.AppContextManager
import com.yubico.authenticator.DialogManager
import com.yubico.authenticator.MainViewModel
import com.yubico.authenticator.NULL
import com.yubico.authenticator.asString
import com.yubico.authenticator.device.DeviceListener
import com.yubico.authenticator.device.DeviceManager
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.device.UnknownDevice
import com.yubico.authenticator.fido.data.FidoCredential
import com.yubico.authenticator.fido.data.FidoFingerprint
import com.yubico.authenticator.fido.data.Session
import com.yubico.authenticator.fido.data.SessionInfo
import com.yubico.authenticator.fido.data.YubiKitFidoSession
@ -38,13 +39,17 @@ import com.yubico.yubikit.core.Transport
import com.yubico.yubikit.core.YubiKeyConnection
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
import com.yubico.yubikit.core.application.CommandState
import com.yubico.yubikit.core.fido.CtapException
import com.yubico.yubikit.core.fido.FidoConnection
import com.yubico.yubikit.core.internal.Logger
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.util.Result
import com.yubico.yubikit.fido.ctap.BioEnrollment
import com.yubico.yubikit.fido.ctap.ClientPin
import com.yubico.yubikit.fido.ctap.CredentialManagement
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1
@ -72,6 +77,12 @@ class FidoManager(
dialogManager: DialogManager,
) : AppContextManager(), DeviceListener {
@OptIn(ExperimentalStdlibApi::class)
private object HexCodec {
fun bytesToHexString(bytes: ByteArray) : String = bytes.toHexString()
fun hexStringToBytes(hex: String) : ByteArray = hex.hexToByteArray()
}
companion object {
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
@ -135,6 +146,21 @@ class FidoManager(
args["credentialId"] as String
)
"deleteFingerprint" -> deleteFingerprint(
args["templateId"] as String
)
"renameFingerprint" -> renameFingerprint(
args["templateId"] as String,
args["name"] as String
)
"registerFingerprint" -> registerFingerprint(
args["name"] as String?,
)
"cancelRegisterFingerprint" -> cancelRegisterFingerprint()
else -> throw NotImplementedError()
}
}
@ -215,8 +241,7 @@ class FidoManager(
fidoViewModel.setSessionState(
Session(
fidoSession.cachedInfo,
pinStore.hasPin()
fidoSession.cachedInfo, pinStore.hasPin()
)
)
@ -234,12 +259,14 @@ class FidoManager(
}
}
private fun getPermissions(fidoSession: YubiKitFidoSession): Int {
// TODO: Add bio Enrollment permissions if supported
private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int {
return if (CredentialManagement.isSupported(fidoSession.cachedInfo))
ClientPin.PIN_PERMISSION_CM
else
0
ClientPin.PIN_PERMISSION_CM else 0
}
private fun getPinPermissionsBE(fidoSession: YubiKitFidoSession): Int {
return if (BioEnrollment.isSupported(fidoSession.cachedInfo))
ClientPin.PIN_PERMISSION_BE else 0
}
private fun unlockSession(
@ -250,13 +277,21 @@ class FidoManager(
//fidoViewModel.setSessionLoadingState()
val permissions = getPermissions(fidoSession)
val pinPermissionsCM = getPinPermissionsCM(fidoSession)
val pinPermissionsBE = getPinPermissionsBE(fidoSession)
val permissions = pinPermissionsCM or pinPermissionsBE
if (permissions != 0) {
val token = clientPin.getPinToken(pin, permissions, null)
val credentials = getCredentials(fidoSession, clientPin, token)
logger.debug("Creds: {}", credentials)
fidoViewModel.updateCredentials(credentials)
if (pinPermissionsBE != 0) {
val fingerprints = getFingerprints(fidoSession, clientPin, token)
logger.debug("Fingerprints: {}", fingerprints)
fidoViewModel.updateFingerprints(fingerprints)
}
} else {
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
}
@ -384,10 +419,8 @@ class FidoManager(
val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
val permissions = getPermissions(fidoSession)
val permissions = getPinPermissionsCM(fidoSession)
val token = clientPin.getPinToken(pinStore.getPin(), permissions, null)
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
val credentialDescriptor =
@ -413,6 +446,153 @@ class FidoManager(
).toString()
}
private fun getFingerprints(
fidoSession: YubiKitFidoSession,
clientPin: ClientPin,
pinUvAuthToken: ByteArray
): List<FidoFingerprint> {
val bioEnrollment =
FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
val enrollments: Map<ByteArray, String?> = bioEnrollment.enumerateEnrollments()
return enrollments.map { enrollment ->
FidoFingerprint(HexCodec.bytesToHexString(enrollment.key), enrollment.value)
}
}
private suspend fun deleteFingerprint(templateId: String): String =
connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession ->
val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
val token =
clientPin.getPinToken(
pinStore.getPin(),
getPinPermissionsBE(fidoSession),
null
)
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId))
fidoViewModel.removeFingerprint(templateId)
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
return@useSession JSONObject(
mapOf(
"success" to true,
)
).toString()
}
private suspend fun renameFingerprint(templateId: String, name: String): String =
connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession ->
val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
val token =
clientPin.getPinToken(
pinStore.getPin(),
getPinPermissionsBE(fidoSession),
null
)
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name)
fidoViewModel.renameFingerprint(templateId, name)
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
return@useSession JSONObject(
mapOf(
"success" to true,
)
).toString()
}
private var state : CommandState? = null
private fun cancelRegisterFingerprint(): String {
state?.cancel()
return NULL
}
private suspend fun registerFingerprint(name: String?): String =
connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession ->
state?.cancel()
state = CommandState()
val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
val token =
clientPin.getPinToken(
pinStore.getPin(),
getPinPermissionsBE(fidoSession),
null
)
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
val fingerprintEnrollmentContext = bioEnrollment.enroll(null)
var templateId: ByteArray? = null
while (templateId == null) {
try {
templateId = fingerprintEnrollmentContext.capture(state)
fidoViewModel.updateRegisterFpState(
createCaptureEvent(fingerprintEnrollmentContext.remaining!!)
)
} catch (captureError: FingerprintBioEnrollment.CaptureError) {
fidoViewModel.updateRegisterFpState(createCaptureErrorEvent(captureError.code))
} catch (ctapException: CtapException) {
when (ctapException.ctapError) {
CtapException.ERR_KEEPALIVE_CANCEL -> {
fingerprintEnrollmentContext.cancel()
return@useSession JSONObject(
mapOf(
"success" to false,
"status" to "user-cancelled"
)
).toString()
}
CtapException.ERR_USER_ACTION_TIMEOUT -> {
fingerprintEnrollmentContext.cancel()
return@useSession JSONObject(
mapOf(
"success" to false,
"status" to "user-action-timeout"
)
).toString()
}
else -> throw ctapException
}
} catch (io: IOException) {
return@useSession JSONObject(
mapOf(
"success" to false,
"status" to "connection-error"
)
).toString()
}
}
if (!name.isNullOrBlank()) {
bioEnrollment.setName(templateId, name)
Logger.debug(logger, "Set name to {}", name)
}
val templateIdHexString = HexCodec.bytesToHexString(templateId)
fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name))
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
return@useSession JSONObject(
mapOf(
"success" to true,
"template_id" to templateIdHexString,
"name" to name
)
).toString()
}
override fun onDisconnected() {
if (!resetHelper.inProgress) {
fidoViewModel.clearSessionState()

View File

@ -29,6 +29,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.Timer
@ -46,6 +48,24 @@ enum class FidoResetState(val value: String) {
Touch("touch")
}
@Serializable
sealed class FidoRegisterFpEvent(val status: String)
@Serializable
data class FidoRegisterFpCaptureEvent(private val remaining: Int) : FidoRegisterFpEvent("capture")
@Serializable
data class FidoRegisterFpCaptureErrorEvent(val code: Int) : FidoRegisterFpEvent("capture-error")
fun createCaptureEvent(remaining: Int): FidoRegisterFpCaptureEvent {
return FidoRegisterFpCaptureEvent(remaining)
}
fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent {
return FidoRegisterFpCaptureErrorEvent(code)
}
class FidoResetHelper(
private val deviceManager: DeviceManager,
private val fidoViewModel: FidoViewModel,

View File

@ -21,7 +21,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.yubico.authenticator.ViewModelData
import com.yubico.authenticator.fido.data.FidoCredential
import com.yubico.authenticator.fido.data.FidoFingerprint
import com.yubico.authenticator.fido.data.Session
import org.json.JSONObject
class FidoViewModel : ViewModel() {
private val _sessionState = MutableLiveData<ViewModelData>()
@ -60,4 +62,36 @@ class FidoViewModel : ViewModel() {
fun updateResetState(resetState: FidoResetState) {
_resetState.postValue(resetState.value)
}
private val _fingerprints = MutableLiveData<List<FidoFingerprint>>()
val fingerprints: LiveData<List<FidoFingerprint>> = _fingerprints
fun updateFingerprints(fingerprints: List<FidoFingerprint>) {
_fingerprints.postValue(fingerprints)
}
fun addFingerprint(fingerprint: FidoFingerprint) {
_fingerprints.postValue(_fingerprints.value?.plus(fingerprint))
}
fun removeFingerprint(templateId: String) {
_fingerprints.postValue(_fingerprints.value?.filter {
it.templateId != templateId
})
}
fun renameFingerprint(templateId: String, name: String) {
_fingerprints.postValue(_fingerprints.value?.map {
if (it.templateId == templateId) {
FidoFingerprint(templateId, name)
} else it
})
}
private val _registerFingerprint = MutableLiveData<FidoRegisterFpEvent>()
val registerFingerprint: LiveData<FidoRegisterFpEvent> = _registerFingerprint
fun updateRegisterFpState(registerFpState: FidoRegisterFpEvent) {
_registerFingerprint.postValue(registerFpState)
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yubico.authenticator.fido.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FidoFingerprint(
@SerialName("template_id")
val templateId: String,
@SerialName("name")
val name: String?
)

View File

@ -25,6 +25,7 @@ import '../../app/logging.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../desktop/models.dart';
import '../../exception/cancellation_exception.dart';
import '../../exception/no_data_exception.dart';
import '../../exception/platform_exception_decoder.dart';
@ -165,14 +166,91 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
_FidoFingerprintsNotifier.new);
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
final _events = const EventChannel('android.fido.fingerprints');
late StreamSubscription _sub;
@override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
return [];
_sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event);
if (json == null) {
state = const AsyncValue.loading();
} else {
List<Fingerprint> newState = List.from(
(json as List).map((e) => Fingerprint.fromJson(e)).toList());
state = AsyncValue.data(newState);
}
}, onError: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace);
});
ref.onDispose(_sub.cancel);
return Completer<List<Fingerprint>>().future;
}
@override
Stream<FingerprintEvent> registerFingerprint({String? name}) {
final controller = StreamController<FingerprintEvent>();
const registerEvents = EventChannel('android.fido.registerFp');
final registerFpSub =
registerEvents.receiveBroadcastStream().skip(1).listen((event) {
if (controller.isClosed) {
_log.debug('Controller already closed, ignoring: $event');
}
_log.debug('Received register fingerprint event: $event');
if (event is String && event.isNotEmpty) {
final e = jsonDecode(event);
_log.debug('Received register fingerprint event: $e');
final status = e['status'];
controller.sink.add(switch (status) {
'capture' => FingerprintEvent.capture(e['remaining']),
'capture-error' => FingerprintEvent.error(e['code']),
final other => throw UnimplementedError(other)
});
}
});
controller.onCancel = () async {
if (!controller.isClosed) {
_log.debug('Cancelling fingerprint registration');
await _methods.invokeMethod('cancelRegisterFingerprint');
await registerFpSub.cancel();
}
};
controller.onListen = () async {
try {
final registerFpResult =
await _methods.invokeMethod('registerFingerprint', {'name': name});
_log.debug('Finished registerFingerprint with: $registerFpResult');
final resultJson = jsonDecode(registerFpResult);
if (resultJson['success'] == true) {
controller.sink
.add(FingerprintEvent.complete(Fingerprint.fromJson(resultJson)));
} else {
// TODO abstract platform errors
final errorStatus = resultJson['status'];
if (errorStatus != 'user-cancelled') {
throw RpcError(errorStatus, 'Platform error: $errorStatus', {});
}
}
} on PlatformException catch (pe) {
_log.debug('Received platform exception: \'$pe\'');
final decoded = pe.decode();
controller.sink.addError(decoded);
} catch (e) {
_log.debug('Received error: \'$e\'');
controller.sink.addError(e);
} finally {
await controller.sink.close();
}
};
return controller.stream;
}
@ -180,11 +258,60 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
@override
Future<Fingerprint> renameFingerprint(
Fingerprint fingerprint, String name) async {
return fingerprint;
try {
final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod(
'renameFingerprint',
{
'templateId': fingerprint.templateId,
'name': name,
},
));
if (renameFingerprintResponse['success'] == true) {
_log.debug('FIDO rename fingerprint succeeded');
return Fingerprint(fingerprint.templateId, name);
} else {
_log.debug('FIDO rename fingerprint failed');
return fingerprint;
}
} on PlatformException catch (pe) {
var decodedException = pe.decode();
if (decodedException is CancellationException) {
_log.debug('User cancelled rename fingerprint FIDO operation');
} else {
_log.error('Rename fingerprint FIDO operation failed.', pe);
}
throw decodedException;
}
}
@override
Future<void> deleteFingerprint(Fingerprint fingerprint) async {}
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
try {
final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod(
'deleteFingerprint',
{
'templateId': fingerprint.templateId,
},
));
if (deleteFingerprintResponse['success'] == true) {
_log.debug('FIDO delete fingerprint succeeded');
} else {
_log.debug('FIDO delete fingerprint failed');
}
} on PlatformException catch (pe) {
var decodedException = pe.decode();
if (decodedException is CancellationException) {
_log.debug('User cancelled delete fingerprint FIDO operation');
} else {
_log.error('Delete fingerprint FIDO operation failed.', pe);
}
throw decodedException;
}
}
}
final androidCredentialProvider = AsyncNotifierProvider.autoDispose

View File

@ -85,12 +85,20 @@ Future<Widget> initialize() async {
),
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
supportedSectionsProvider.overrideWithValue(
[Section.home, Section.accounts, Section.passkeys]),
supportedSectionsProvider.overrideWithValue([
Section.home,
Section.accounts,
Section.fingerprints,
Section.passkeys
]),
// this specifies the priority of sections to show when
// the connected YubiKey does not support current section
androidSectionPriority.overrideWithValue(
[Section.accounts, Section.passkeys, Section.home]),
androidSectionPriority.overrideWithValue([
Section.accounts,
Section.fingerprints,
Section.passkeys,
Section.home
]),
supportedThemesProvider.overrideWith(
(ref) => ref.watch(androidSupportedThemesProvider),
),

View File

@ -78,6 +78,8 @@ enum _DDesc {
fidoUnlockSession,
fidoSetPin,
fidoDeleteCredential,
fidoDeleteFingerprint,
fidoRenameFingerprint,
fidoActionFailure,
// Others
invalid;
@ -101,7 +103,9 @@ enum _DDesc {
dialogDescriptionFidoIndex + 1: fidoUnlockSession,
dialogDescriptionFidoIndex + 2: fidoSetPin,
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
dialogDescriptionFidoIndex + 4: fidoActionFailure,
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
dialogDescriptionFidoIndex + 6: fidoActionFailure,
}[id] ??
_DDesc.invalid;
}
@ -179,6 +183,8 @@ class _DialogProvider {
_DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock,
_DDesc.fidoSetPin => l10n.l_nfc_dialog_fido_set_pin,
_DDesc.fidoDeleteCredential => l10n.s_nfc_dialog_fido_delete_credential,
_DDesc.fidoDeleteFingerprint => l10n.s_nfc_dialog_fido_delete_fingerprint,
_DDesc.fidoRenameFingerprint => l10n.s_nfc_dialog_fido_rename_fingerprint,
_DDesc.fidoActionFailure => l10n.s_nfc_dialog_fido_failure,
_ => ''
};

View File

@ -63,6 +63,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
void dispose() {
_animator.dispose();
_nameFocus.dispose();
_subscription.cancel();
super.dispose();
}

View File

@ -838,6 +838,8 @@
"s_nfc_dialog_fido_unlock": null,
"l_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_fingerprint": null,
"s_nfc_dialog_fido_rename_fingerprint": null,
"s_nfc_dialog_fido_failure": null,
"@_ndef": {},

View File

@ -838,6 +838,8 @@
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO applet",
"l_nfc_dialog_fido_set_pin": "Action: set or change FIDO applet PIN",
"s_nfc_dialog_fido_delete_credential": "Action: delete Passkey",
"s_nfc_dialog_fido_delete_fingerprint": "Action: delete fingerprint",
"s_nfc_dialog_fido_rename_fingerprint": "Action: rename fingerprint",
"s_nfc_dialog_fido_failure": "FIDO operation failed",
"@_ndef": {},

View File

@ -838,6 +838,8 @@
"s_nfc_dialog_fido_unlock": null,
"l_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_fingerprint": null,
"s_nfc_dialog_fido_rename_fingerprint": null,
"s_nfc_dialog_fido_failure": null,
"@_ndef": {},

View File

@ -838,6 +838,8 @@
"s_nfc_dialog_fido_unlock": null,
"l_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_fingerprint": null,
"s_nfc_dialog_fido_rename_fingerprint": null,
"s_nfc_dialog_fido_failure": null,
"@_ndef": {},

View File

@ -838,6 +838,8 @@
"s_nfc_dialog_fido_unlock": null,
"l_nfc_dialog_fido_set_pin": null,
"s_nfc_dialog_fido_delete_credential": null,
"s_nfc_dialog_fido_delete_fingerprint": null,
"s_nfc_dialog_fido_rename_fingerprint": null,
"s_nfc_dialog_fido_failure": null,
"@_ndef": {},