FIDO unlock & setPin

This commit is contained in:
Adam Velebil 2024-01-02 17:52:35 +01:00
parent c2624592cd
commit 7254e8ef10
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
13 changed files with 716 additions and 21 deletions

View File

@ -99,7 +99,6 @@ dependencies {
api "com.yubico.yubikit:fido:$project.yubiKitVersion"
api "com.yubico.yubikit:support:$project.yubiKitVersion"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
// Lifecycle
@ -109,7 +108,7 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.github.tony19:logback-android:3.0.0'

View File

@ -39,6 +39,8 @@ import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.color.DynamicColors
import com.yubico.authenticator.fido.FidoManager
import com.yubico.authenticator.fido.FidoViewModel
import com.yubico.authenticator.logging.FlutterLog
import com.yubico.authenticator.oath.AppLinkMethodChannel
import com.yubico.authenticator.oath.OathManager
@ -62,6 +64,7 @@ import java.util.concurrent.Executors
class MainActivity : FlutterFragmentActivity() {
private val viewModel: MainViewModel by viewModels()
private val oathViewModel: OathViewModel by viewModels()
private val fidoViewModel: FidoViewModel by viewModels()
private val nfcConfiguration = NfcConfiguration()
@ -298,6 +301,7 @@ class MainActivity : FlutterFragmentActivity() {
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"),
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
)
viewModel.appContext.observe(this) {
@ -311,6 +315,14 @@ class MainActivity : FlutterFragmentActivity() {
dialogManager,
appPreferences
)
OperationContext.Fido -> FidoManager(
this,
messenger,
viewModel,
fidoViewModel,
dialogManager,
appPreferences
)
else -> null
}
viewModel.connectedYubiKey.value?.let(::processYubiKey)

View File

@ -23,26 +23,33 @@ import com.yubico.authenticator.device.Info
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
enum class OperationContext(val value: Int) {
Oath(0), Yubikey(1), Invalid(-1);
Oath(0),
Fido(1),
YubiOtp(2),
Piv(3),
OpenPgp(4),
HsmAuth(5),
Management(6),
Invalid(-1);
companion object {
fun getByValue(value: Int) = values().firstOrNull { it.value == value } ?: Invalid
fun getByValue(value: Int) = entries.firstOrNull { it.value == value } ?: Invalid
}
}
class MainViewModel : ViewModel() {
private var _appContext = MutableLiveData(OperationContext.Oath)
private var _appContext = MutableLiveData(OperationContext.Fido)
val appContext: LiveData<OperationContext> = _appContext
fun setAppContext(appContext: OperationContext) {
// Don't reset the context unless it actually changes
if(appContext != _appContext.value) {
if (appContext != _appContext.value) {
_appContext.postValue(appContext)
}
}
private val _connectedYubiKey = MutableLiveData<UsbYubiKeyDevice?>()
val connectedYubiKey: LiveData<UsbYubiKeyDevice?> = _connectedYubiKey
fun setConnectedYubiKey(device: UsbYubiKeyDevice, onDisconnect: () -> Unit ) {
fun setConnectedYubiKey(device: UsbYubiKeyDevice, onDisconnect: () -> Unit) {
_connectedYubiKey.postValue(device)
device.setOnClosed {
_connectedYubiKey.postValue(null)

View File

@ -0,0 +1,13 @@
package com.yubico.authenticator.fido
const val dialogDescriptionOathIndex = 200
enum class FidoActionDescription(private val value: Int) {
Reset(0),
Unlock(1),
SetPin(2),
ActionFailure(3);
val id: Int
get() = value + dialogDescriptionOathIndex
}

View File

@ -0,0 +1,394 @@
package com.yubico.authenticator.fido
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.yubico.authenticator.AppContextManager
import com.yubico.authenticator.AppPreferences
import com.yubico.authenticator.DialogIcon
import com.yubico.authenticator.DialogManager
import com.yubico.authenticator.DialogTitle
import com.yubico.authenticator.MainViewModel
import com.yubico.authenticator.asString
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.device.UnknownDevice
import com.yubico.authenticator.fido.data.Session
import com.yubico.authenticator.fido.data.YubiKitFidoSession
import com.yubico.authenticator.setHandler
import com.yubico.authenticator.yubikit.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.Transport
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
import com.yubico.yubikit.core.fido.CtapException
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.util.Result
import com.yubico.yubikit.fido.ctap.ClientPin
import com.yubico.yubikit.fido.ctap.CredentialManagement
import com.yubico.yubikit.fido.ctap.Ctap2Session
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2
import com.yubico.yubikit.support.DeviceUtil
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.util.Arrays
import java.util.concurrent.Executors
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.suspendCoroutine
typealias FidoAction = (Result<YubiKitFidoSession, Exception>) -> Unit
class FidoManager(
private val lifecycleOwner: LifecycleOwner,
messenger: BinaryMessenger,
private val appViewModel: MainViewModel,
private val fidoViewModel: FidoViewModel,
private val dialogManager: DialogManager,
private val appPreferences: AppPreferences,
) : AppContextManager {
companion object {
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
val pinSupported = infoData.options["clientPin"] != null
if (pinSupported) {
for (protocol in pinUvAuthProtocols) {
if (protocol == PinUvAuthProtocolV1.VERSION) {
return PinUvAuthProtocolV1()
}
if (protocol == PinUvAuthProtocolV2.VERSION) {
return PinUvAuthProtocolV2()
}
}
}
return PinUvAuthDummyProtocol()
}
}
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
private val fidoChannel = MethodChannel(messenger, "android.fido.methods")
private val logger = LoggerFactory.getLogger(FidoManager::class.java)
private var pendingAction: FidoAction? = null
private var token: ByteArray? = null
private val lifecycleObserver = object : DefaultLifecycleObserver {
private var startTimeMs: Long = -1
override fun onPause(owner: LifecycleOwner) {
startTimeMs = currentTimeMs
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
if (canInvoke) {
if (appViewModel.connectedYubiKey.value == null) {
// no USB YubiKey is connected, reset known data on resume
logger.debug("Removing NFC data after resume.")
appViewModel.setDeviceInfo(null)
fidoViewModel.setSessionState(null)
}
}
}
private val currentTimeMs
get() = System.currentTimeMillis()
private val canInvoke: Boolean
get() = startTimeMs != -1L && currentTimeMs - startTimeMs > NFC_DATA_CLEANUP_DELAY
}
private val usbObserver = Observer<UsbYubiKeyDevice?> {
if (it == null) {
appViewModel.setDeviceInfo(null)
fidoViewModel.setSessionState(null)
}
}
init {
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
//fidoViewModel.credentials.observe(lifecycleOwner, credentialObserver)
// FIDO methods callable from Flutter:
fidoChannel.setHandler(coroutineScope) { method, args ->
when (method) {
"reset" -> noop()
"unlock" -> unlock(
(args["pin"] as String).toCharArray()
)
"set_pin" -> setPin(
(args["pin"] as String?)?.toCharArray(),
(args["new_pin"] as String).toCharArray(),
)
else -> throw NotImplementedError()
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
}
override fun dispose() {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
appViewModel.connectedYubiKey.removeObserver(usbObserver)
// oathViewModel.credentials.removeObserver(credentialObserver)
fidoChannel.setMethodCallHandler(null)
coroutineScope.cancel()
}
private fun noop(): String = ""
override suspend fun processYubiKey(device: YubiKeyDevice) {
try {
device.withConnection<SmartCardConnection, Unit> { connection ->
val session = YubiKitFidoSession(connection)
val previousAaguid = fidoViewModel.sessionState.value?.info?.aaguid?.asString()
val sessionAaguid = session.cachedInfo.aaguid.asString()
logger.debug(
"Previous aaguid: {}, current aaguid: {}",
previousAaguid,
sessionAaguid
)
if (sessionAaguid == previousAaguid && device is NfcYubiKeyDevice) {
// Run any pending action
pendingAction?.let { action ->
action.invoke(Result.success(session))
pendingAction = null
}
} else {
if (sessionAaguid != previousAaguid) {
// different key
logger.debug("This is a different key than previous, invalidating the PIN token")
if (token != null) {
Arrays.fill(token!!, 0.toByte())
token = null
}
}
fidoViewModel.setSessionState(
Session(
session,
token != null
)
)
// Update deviceInfo since the deviceId has changed
val pid = (device as? UsbYubiKeyDevice)?.pid
val deviceInfo = DeviceUtil.readInfo(connection, pid)
appViewModel.setDeviceInfo(
Info(
name = DeviceUtil.getName(deviceInfo, pid?.type),
isNfc = device.transport == Transport.NFC,
usbPid = pid?.value,
deviceInfo = deviceInfo
)
)
}
}
} catch (e: Exception) {
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Failed to connect to CCID", e)
if (device.transport == Transport.USB || e is ApplicationNotAvailableException) {
val deviceInfo = try {
getDeviceInfo(device)
} catch (e: IllegalArgumentException) {
logger.debug("Device was not recognized")
UnknownDevice.copy(isNfc = device.transport == Transport.NFC)
} catch (e: Exception) {
logger.error("Failure getting device info", e)
null
}
logger.debug("Setting device info: {}", deviceInfo)
appViewModel.setDeviceInfo(deviceInfo)
}
// Clear any cached OATH state
fidoViewModel.setSessionState(null)
} finally {
}
}
private fun unlockSession(
ctap2Session: Ctap2Session,
clientPin: ClientPin,
pin: CharArray
): String {
val permissions =
if (CredentialManagement.isSupported(ctap2Session.cachedInfo))
ClientPin.PIN_PERMISSION_CM
else
0
// TODO: Add bio Enrollment permissions if supported
if (permissions != 0) {
token = clientPin.getPinToken(pin, permissions, "")
} else {
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
}
fidoViewModel.setSessionState(
Session(
ctap2Session,
token != null
)
)
return JSONObject(mapOf("success" to true)).toString()
}
private fun catchPinErrors(clientPin: ClientPin, block: () -> String): String =
try {
block()
} catch (ctapException: CtapException) {
if (ctapException.ctapError == CtapException.ERR_PIN_INVALID ||
ctapException.ctapError == CtapException.ERR_PIN_BLOCKED ||
ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED
) {
token = null
val pinRetriesResult = clientPin.pinRetries
JSONObject(
mapOf(
"success" to false,
"pinRetries" to pinRetriesResult.count,
"authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED)
)
).toString()
} else {
throw ctapException
}
}
private suspend fun unlock(pin: CharArray): String =
useSession(FidoActionDescription.Unlock) { ctap2Session ->
val clientPin =
ClientPin(ctap2Session, getPreferredPinUvAuthProtocol(ctap2Session.cachedInfo))
try {
catchPinErrors(clientPin) {
unlockSession(ctap2Session, clientPin, pin)
}
} finally {
Arrays.fill(pin, 0.toChar())
}
}
private fun setOrChangePin(
ctap2Session: Ctap2Session,
clientPin: ClientPin,
pin: CharArray?,
newPin: CharArray
) {
val infoData = ctap2Session.cachedInfo
val hasPin = infoData.options["clientPin"] == true
if (hasPin) {
clientPin.changePin(pin!!, newPin)
} else {
clientPin.setPin(newPin)
}
}
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
useSession(FidoActionDescription.SetPin) { ctap2Session ->
val clientPin =
ClientPin(ctap2Session, getPreferredPinUvAuthProtocol(ctap2Session.cachedInfo))
try {
catchPinErrors(clientPin) {
setOrChangePin(ctap2Session, clientPin, pin, newPin)
unlockSession(ctap2Session, clientPin, newPin)
}
} finally {
Arrays.fill(newPin, 0.toChar())
pin?.let {
Arrays.fill(it, 0.toChar())
}
}
}
private suspend fun <T> useSession(
actionDescription: FidoActionDescription,
action: (YubiKitFidoSession) -> T
): T {
return appViewModel.connectedYubiKey.value?.let {
useSessionUsb(it, action)
} ?: useSessionNfc(actionDescription, action)
}
private suspend fun <T> useSessionUsb(
device: UsbYubiKeyDevice,
block: (YubiKitFidoSession) -> T
): T = device.withConnection<SmartCardConnection, T> {
block(YubiKitFidoSession(it))
}
private suspend fun <T> useSessionNfc(
actionDescription: FidoActionDescription,
block: (YubiKitFidoSession) -> T
): T {
try {
val result = suspendCoroutine { outer ->
pendingAction = {
outer.resumeWith(runCatching {
block.invoke(it.value)
})
}
dialogManager.showDialog(
DialogIcon.Nfc,
DialogTitle.TapKey,
actionDescription.id
) {
logger.debug("Cancelled Dialog {}", actionDescription.name)
pendingAction?.invoke(Result.failure(CancellationException()))
pendingAction = null
}
}
// Personally I find it better to not have the dialog updates for FIDO
// dialogManager.updateDialogState(
// dialogIcon = DialogIcon.Success,
// dialogTitle = DialogTitle.OperationSuccessful
// )
// // TODO: This delays the closing of the dialog, but also the return value
// delay(500)
return result
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Throwable) {
// Personally I find it better to not have the dialog updates for FIDO
// dialogManager.updateDialogState(
// dialogIcon = DialogIcon.Failure,
// dialogTitle = DialogTitle.OperationFailed,
// dialogDescriptionId = FidoActionDescription.ActionFailure.id
// )
// // TODO: This delays the closing of the dialog, but also the return value
// delay(1500)
throw error
} finally {
dialogManager.closeDialog()
}
}
}

View File

@ -0,0 +1,16 @@
package com.yubico.authenticator.fido
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.yubico.authenticator.fido.data.Session
class FidoViewModel : ViewModel() {
private val _sessionState = MutableLiveData<Session?>()
val sessionState: LiveData<Session?> = _sessionState
fun setSessionState(sessionState: Session?) {
_sessionState.postValue(sessionState)
}
}

View File

@ -0,0 +1,80 @@
package com.yubico.authenticator.fido.data
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
import kotlinx.serialization.*
typealias YubiKitFidoSession = com.yubico.yubikit.fido.ctap.Ctap2Session
@Serializable
data class Options(
val clientPin: Boolean,
val credMgmt: Boolean,
val credentialMgmtPreview: Boolean,
val bioEnroll: Boolean?,
val alwaysUv: Boolean
)
fun Map<String, Any?>.getBoolean(
key: String,
default: Boolean = false
): Boolean = get(key) as? Boolean ?: default
fun Map<String, Any?>.getOptionalBoolean(
key: String
): Boolean? = get(key) as? Boolean
@Serializable
data class SessionInfo(
val options: Options,
val aaguid: ByteArray,
@SerialName("min_pin_length")
val minPinLength: Int,
@SerialName("force_pin_change")
val forcePinChange: Boolean
) {
constructor(infoData: InfoData) : this(
Options(
infoData.options.getBoolean("clientPin"),
infoData.options.getBoolean("credMgmt"),
infoData.options.getBoolean("credentialMgmtPreview"),
infoData.options.getOptionalBoolean("bioEnroll"),
infoData.options.getBoolean("alwaysUv")
),
infoData.aaguid,
infoData.minPinLength,
infoData.forcePinChange
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SessionInfo
if (options != other.options) return false
if (!aaguid.contentEquals(other.aaguid)) return false
if (minPinLength != other.minPinLength) return false
return forcePinChange == other.forcePinChange
}
override fun hashCode(): Int {
var result = options.hashCode()
result = 31 * result + aaguid.contentHashCode()
result = 31 * result + minPinLength
result = 31 * result + forcePinChange.hashCode()
return result
}
}
@Serializable
data class Session(
@SerialName("info")
val info: SessionInfo,
@SerialName("unlocked")
val unlocked: Boolean
) {
constructor(fidoSession: YubiKitFidoSession, unlocked: Boolean) : this(
SessionInfo(fidoSession.cachedInfo), unlocked
)
}

View File

@ -50,6 +50,7 @@ import com.yubico.yubikit.core.Transport
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
import com.yubico.yubikit.core.smartcard.ApduException
import com.yubico.yubikit.core.smartcard.AppId
import com.yubico.yubikit.core.smartcard.SW
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
@ -79,7 +80,6 @@ class OathManager(
) : AppContextManager {
companion object {
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
val OTP_AID = byteArrayOf(0xa0.toByte(), 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01)
}
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
@ -302,7 +302,7 @@ class OathManager(
if (session.version.isLessThan(4, 0, 0) && connection.transport == Transport.NFC) {
// NEO over NFC, select OTP applet before reading info
try {
SmartCardProtocol(connection).select(OTP_AID)
SmartCardProtocol(connection).select(AppId.OTP)
} catch (e: Exception) {
logger.error("Failed to recognize this OATH device.")
// we know this is NFC device and it supports OATH

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.9.21'
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.4'
classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'

146
lib/android/fido/state.dart Normal file
View File

@ -0,0 +1,146 @@
/*
* Copyright (C) 2023 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.
*/
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import '../../app/logging.dart';
import '../../app/models.dart';
import '../../fido/models.dart';
import '../../fido/state.dart';
final _log = Logger('android.fido.state');
const _methods = MethodChannel('android.fido.methods');
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
class _FidoStateNotifier extends FidoStateNotifier {
late StateController<String?> _pinController;
final _events = const EventChannel('android.fido.sessionState');
late StreamSubscription _sub;
@override
FutureOr<FidoState> build(DevicePath devicePath) async {
_sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event);
if (json == null) {
state = const AsyncValue.loading();
} else {
final fidoState = FidoState.fromJson(json);
state = AsyncValue.data(fidoState);
}
}, onError: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace);
});
ref.onDispose(_sub.cancel);
return Completer<FidoState>().future;
}
@override
Stream<InteractionEvent> reset() {
final controller = StreamController<InteractionEvent>();
return controller.stream;
}
@override
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
try {
final setPinResponse = jsonDecode(await _methods.invokeMethod('set_pin', {
'pin': oldPin,
'new_pin': newPin,
}));
if (setPinResponse['success'] == true) {
_log.debug('FIDO pin set/change successful');
return PinResult.success();
}
_log.debug('FIDO pin set/change failed');
return PinResult.failed(
setPinResponse['pinRetries'], setPinResponse['authBlocked']);
} catch (e) {
rethrow;
}
}
@override
Future<PinResult> unlock(String pin) async {
try {
final unlockResponse =
jsonDecode(await _methods.invokeMethod('unlock', {'pin': pin}));
if (unlockResponse['success'] == true) {
_log.debug('FIDO applet unlocked');
return PinResult.success();
}
_log.debug('FIDO applet unlock failed');
return PinResult.failed(
unlockResponse['pinRetries'], unlockResponse['authBlocked']);
} catch (e) {
rethrow;
}
}
}
final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
.family<FidoFingerprintsNotifier, List<Fingerprint>, DevicePath>(
_FidoFingerprintsNotifier.new);
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
@override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
return [];
}
@override
Stream<FingerprintEvent> registerFingerprint({String? name}) {
final controller = StreamController<FingerprintEvent>();
return controller.stream;
}
@override
Future<Fingerprint> renameFingerprint(
Fingerprint fingerprint, String name) async {
return fingerprint;
}
@override
Future<void> deleteFingerprint(Fingerprint fingerprint) async {}
}
final androidCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
_FidoCredentialsNotifier.new);
class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
@override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
return [];
}
@override
Future<void> deleteCredential(FidoCredential credential) async {}
}

View File

@ -30,9 +30,11 @@ import '../app/models.dart';
import '../app/state.dart';
import '../app/views/main_page.dart';
import '../core/state.dart';
import '../fido/state.dart';
import '../management/state.dart';
import '../oath/state.dart';
import 'app_methods.dart';
import 'fido/state.dart';
import 'logger.dart';
import 'management/state.dart';
import 'oath/otp_auth_link_handler.dart';
@ -55,6 +57,7 @@ Future<Widget> initialize() async {
overrides: [
supportedAppsProvider.overrideWith(implementedApps([
Application.accounts,
Application.webauthn,
])),
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
logLevelProvider.overrideWith((ref) => AndroidLogger()),
@ -86,6 +89,11 @@ Future<Widget> initialize() async {
(ref) => ref.watch(androidSupportedThemesProvider),
),
defaultColorProvider.overrideWithValue(await getPrimaryColor()),
// FIDO
fidoStateProvider.overrideWithProvider(androidFidoStateProvider.call),
fingerprintProvider.overrideWithProvider(androidFingerprintProvider.call),
credentialProvider.overrideWithProvider(androidCredentialProvider.call),
],
child: DismissKeyboard(
child: YubicoAuthenticatorApp(page: Consumer(

View File

@ -73,22 +73,33 @@ enum _DDesc {
oathCalculateCode,
oathActionFailure,
oathAddMultipleAccounts,
// FIDO descriptions
fidoResetApplet,
fidoUnlockSession,
fidoSetPin,
fidoActionFailure,
// Others
invalid;
static const int dialogDescriptionOathIndex = 100;
static const int dialogDescriptionFidoIndex = 200;
static _DDesc fromId(int? id) =>
const {
dialogDescriptionOathIndex + 0: _DDesc.oathResetApplet,
dialogDescriptionOathIndex + 1: _DDesc.oathUnlockSession,
dialogDescriptionOathIndex + 2: _DDesc.oathSetPassword,
dialogDescriptionOathIndex + 3: _DDesc.oathUnsetPassword,
dialogDescriptionOathIndex + 4: _DDesc.oathAddAccount,
dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount,
dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount,
dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode,
dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure,
dialogDescriptionOathIndex + 9: _DDesc.oathAddMultipleAccounts
dialogDescriptionOathIndex + 0: oathResetApplet,
dialogDescriptionOathIndex + 1: oathUnlockSession,
dialogDescriptionOathIndex + 2: oathSetPassword,
dialogDescriptionOathIndex + 3: oathUnsetPassword,
dialogDescriptionOathIndex + 4: oathAddAccount,
dialogDescriptionOathIndex + 5: oathRenameAccount,
dialogDescriptionOathIndex + 6: oathDeleteAccount,
dialogDescriptionOathIndex + 7: oathCalculateCode,
dialogDescriptionOathIndex + 8: oathActionFailure,
dialogDescriptionOathIndex + 9: oathAddMultipleAccounts,
dialogDescriptionFidoIndex + 0: fidoResetApplet,
dialogDescriptionFidoIndex + 1: fidoUnlockSession,
dialogDescriptionFidoIndex + 2: fidoSetPin,
dialogDescriptionFidoIndex + 3: fidoActionFailure,
}[id] ??
_DDesc.invalid;
}
@ -162,6 +173,10 @@ class _DialogProvider {
_DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure,
_DDesc.oathAddMultipleAccounts =>
l10n.s_nfc_dialog_oath_add_multiple_accounts,
_DDesc.fidoResetApplet => l10n.s_nfc_dialog_fido_reset,
_DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock,
_DDesc.fidoSetPin => l10n.s_nfc_dialog_fido_set_pin,
_DDesc.fidoActionFailure => l10n.s_nfc_dialog_fido_failure,
_ => ''
};
}

View File

@ -722,6 +722,11 @@
"s_nfc_dialog_oath_failure": "OATH operation failed",
"s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts",
"s_nfc_dialog_fido_reset": "Action: reset FIDO applet",
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO applet",
"s_nfc_dialog_fido_set_pin": "Action: set or change FIDO applet PIN",
"s_nfc_dialog_fido_failure": "FIDO operation failed",
"@_ndef": {},
"p_ndef_set_otp": "Successfully copied OTP code from YubiKey to clipboard.",
"p_ndef_set_password": "Successfully copied password from YubiKey to clipboard.",