mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-26 11:43:44 +03:00
switch YubiKey applets
This commit is contained in:
parent
5a6e87028d
commit
b74cbe3bcb
@ -37,7 +37,7 @@ class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, pri
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setContext(subPageIndex: Int): String {
|
||||
private fun setContext(subPageIndex: Int): String {
|
||||
val appContext = OperationContext.getByValue(subPageIndex)
|
||||
appViewModel.setAppContext(appContext)
|
||||
logger.debug("App context is now {}", appContext)
|
||||
|
@ -16,12 +16,37 @@
|
||||
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
|
||||
/**
|
||||
* Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app.
|
||||
*/
|
||||
interface AppContextManager {
|
||||
suspend fun processYubiKey(device: YubiKeyDevice)
|
||||
fun dispose()
|
||||
abstract class AppContextManager(
|
||||
private val lifecycleOwner: LifecycleOwner
|
||||
) {
|
||||
abstract suspend fun processYubiKey(device: YubiKeyDevice)
|
||||
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
onPause()
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
onResume()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
open fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
open fun onPause() {}
|
||||
|
||||
open fun onResume() {}
|
||||
}
|
@ -39,12 +39,14 @@ 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.device.DeviceManager
|
||||
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
|
||||
import com.yubico.authenticator.oath.OathViewModel
|
||||
import com.yubico.authenticator.oath.keystore.ClearingMemProvider
|
||||
import com.yubico.yubikit.android.YubiKitManager
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||
@ -265,8 +267,22 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
private fun processYubiKey(device: YubiKeyDevice) {
|
||||
contextManager?.let {
|
||||
lifecycleScope.launch {
|
||||
lifecycleScope.launch {
|
||||
// verify that current context supports connection provided by the YubiKey
|
||||
// if not, switch to a context which supports the connection
|
||||
val supportedApps = DeviceManager.getSupportedContexts(device)
|
||||
logger.debug("Connected key supports: {}", supportedApps)
|
||||
if (!supportedApps.contains(viewModel.appContext.value)) {
|
||||
val preferredContext = DeviceManager.getPreferredContext(supportedApps)
|
||||
logger.debug(
|
||||
"Current context ({}) is not supported by the key. Using preferred context {}",
|
||||
viewModel.appContext.value,
|
||||
preferredContext
|
||||
)
|
||||
switchContext(preferredContext)
|
||||
}
|
||||
|
||||
contextManager?.let {
|
||||
try {
|
||||
it.processYubiKey(device)
|
||||
} catch (e: Throwable) {
|
||||
@ -277,6 +293,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
private var contextManager: AppContextManager? = null
|
||||
private lateinit var deviceManager: DeviceManager
|
||||
private lateinit var appContext: AppContext
|
||||
private lateinit var dialogManager: DialogManager
|
||||
private lateinit var appPreferences: AppPreferences
|
||||
@ -284,13 +301,14 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
private lateinit var flutterStreams: List<Closeable>
|
||||
private lateinit var appMethodChannel: AppMethodChannel
|
||||
private lateinit var appLinkMethodChannel: AppLinkMethodChannel
|
||||
private lateinit var messenger: BinaryMessenger
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
flutterLog = FlutterLog(messenger)
|
||||
deviceManager = DeviceManager(this, viewModel)
|
||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
appPreferences = AppPreferences(this)
|
||||
@ -307,31 +325,39 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
)
|
||||
|
||||
viewModel.appContext.observe(this) {
|
||||
contextManager?.dispose()
|
||||
contextManager = when (it) {
|
||||
OperationContext.Oath -> OathManager(
|
||||
this,
|
||||
messenger,
|
||||
viewModel,
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences
|
||||
)
|
||||
OperationContext.FidoPasskeys -> FidoManager(
|
||||
this,
|
||||
messenger,
|
||||
viewModel,
|
||||
fidoViewModel,
|
||||
dialogManager
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
switchContext(it)
|
||||
viewModel.connectedYubiKey.value?.let(::processYubiKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchContext(appContext: OperationContext) {
|
||||
contextManager?.dispose()
|
||||
contextManager = when (appContext) {
|
||||
OperationContext.Oath -> OathManager(
|
||||
this,
|
||||
messenger,
|
||||
deviceManager,
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences
|
||||
)
|
||||
|
||||
OperationContext.FidoPasskeys -> FidoManager(
|
||||
this,
|
||||
messenger,
|
||||
deviceManager,
|
||||
fidoViewModel,
|
||||
dialogManager
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
flutterStreams.forEach { it.close() }
|
||||
contextManager?.dispose()
|
||||
deviceManager.dispose()
|
||||
super.cleanUpFlutterEngine(flutterEngine)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,173 @@
|
||||
package com.yubico.authenticator.device
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.fido.FidoConnection
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.fido.ctap.Ctap2Session
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
interface DeviceListener {
|
||||
// a USB device is connected
|
||||
fun onConnected(device: YubiKeyDevice) {}
|
||||
|
||||
// a USB device is disconnected
|
||||
fun onDisconnected() {}
|
||||
|
||||
// the app has been paused for more than DeviceManager.NFC_DATA_CLEANUP_DELAY
|
||||
fun onTimeout() {}
|
||||
}
|
||||
|
||||
class DeviceManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val appViewModel: MainViewModel
|
||||
) {
|
||||
var clearDeviceInfoOnDisconnect: Boolean = true
|
||||
|
||||
private val deviceListeners = HashSet<DeviceListener>()
|
||||
|
||||
fun addDeviceListener(listener: DeviceListener) {
|
||||
deviceListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeDeviceListener(listener: DeviceListener) {
|
||||
deviceListeners.remove(listener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
|
||||
private val logger = LoggerFactory.getLogger(DeviceManager::class.java)
|
||||
|
||||
fun getSupportedContexts(device: YubiKeyDevice) : ArraySet<OperationContext> {
|
||||
|
||||
val operationContexts = ArraySet<OperationContext>()
|
||||
|
||||
if (device.supportsConnection(SmartCardConnection::class.java)) {
|
||||
// try which apps are available
|
||||
device.openConnection(SmartCardConnection::class.java).use {
|
||||
try {
|
||||
OathSession(it)
|
||||
operationContexts.add(OperationContext.Oath)
|
||||
} catch (e: Throwable) { // ignored
|
||||
}
|
||||
|
||||
try {
|
||||
Ctap2Session(it)
|
||||
operationContexts.add(OperationContext.FidoPasskeys)
|
||||
operationContexts.add(OperationContext.FidoFingerprints)
|
||||
} catch (e: Throwable) { // ignored
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (device.supportsConnection(FidoConnection::class.java)) {
|
||||
device.openConnection(FidoConnection::class.java).use {
|
||||
try {
|
||||
Ctap2Session(it)
|
||||
operationContexts.add(OperationContext.FidoPasskeys)
|
||||
operationContexts.add(OperationContext.FidoFingerprints)
|
||||
} catch (e: Throwable) { // ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Device supports following contexts: {}", operationContexts)
|
||||
return operationContexts
|
||||
}
|
||||
|
||||
fun getPreferredContext(contexts: ArraySet<OperationContext>) : OperationContext {
|
||||
// custom sort
|
||||
for(context in contexts) {
|
||||
if (context == OperationContext.Oath) {
|
||||
return context
|
||||
} else if (context == OperationContext.FidoPasskeys) {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return OperationContext.Oath
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
if (clearDeviceInfoOnDisconnect) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
}
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val currentTimeMs
|
||||
get() = System.currentTimeMillis()
|
||||
|
||||
private val canInvoke: Boolean
|
||||
get() = startTimeMs != -1L && currentTimeMs - startTimeMs > NFC_DATA_CLEANUP_DELAY
|
||||
}
|
||||
|
||||
private val usbObserver = Observer<UsbYubiKeyDevice?> { yubiKeyDevice ->
|
||||
if (yubiKeyDevice == null) {
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onDisconnected()
|
||||
}
|
||||
if (clearDeviceInfoOnDisconnect) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
}
|
||||
} else {
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onConnected(yubiKeyDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
}
|
||||
|
||||
fun setDeviceInfo(deviceInfo: Info?) {
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
}
|
||||
|
||||
fun isUsbKeyConnected(): Boolean {
|
||||
return appViewModel.connectedYubiKey.value != null
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
} ?: onNfc()
|
||||
}
|
@ -20,6 +20,7 @@ import com.yubico.authenticator.DialogIcon
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.DialogTitle
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
@ -30,7 +31,7 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class FidoConnectionHelper(
|
||||
private val appViewModel: MainViewModel,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val dialogManager: DialogManager
|
||||
) {
|
||||
private var pendingAction: FidoAction? = null
|
||||
@ -46,9 +47,9 @@ class FidoConnectionHelper(
|
||||
actionDescription: FidoActionDescription,
|
||||
action: (YubiKitFidoSession) -> T
|
||||
): T {
|
||||
return appViewModel.connectedYubiKey.value?.let {
|
||||
useSessionUsb(it, action)
|
||||
} ?: useSessionNfc(actionDescription, action)
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(actionDescription,action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionUsb(
|
||||
|
@ -16,13 +16,12 @@
|
||||
|
||||
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.DialogManager
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
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
|
||||
@ -63,16 +62,14 @@ import java.util.concurrent.Executors
|
||||
typealias FidoAction = (Result<YubiKitFidoSession, Exception>) -> Unit
|
||||
|
||||
class FidoManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
messenger: BinaryMessenger,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
dialogManager: DialogManager,
|
||||
) : AppContextManager {
|
||||
) : AppContextManager(lifecycleOwner), DeviceListener {
|
||||
|
||||
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
|
||||
@ -90,7 +87,7 @@ class FidoManager(
|
||||
}
|
||||
}
|
||||
|
||||
private val connectionHelper = FidoConnectionHelper(appViewModel, dialogManager)
|
||||
private val connectionHelper = FidoConnectionHelper(deviceManager, dialogManager)
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
@ -102,50 +99,15 @@ class FidoManager(
|
||||
private val pinStore = FidoPinStore()
|
||||
|
||||
private val resetHelper =
|
||||
FidoResetHelper(appViewModel, fidoViewModel, connectionHelper, pinStore)
|
||||
FidoResetHelper(deviceManager, fidoViewModel, connectionHelper, pinStore)
|
||||
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
|
||||
private var startTimeMs: Long = -1
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
// cancel any FIDO reset flow which might be in progress
|
||||
resetHelper.cancelReset()
|
||||
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) {
|
||||
if (!resetHelper.inProgress) {
|
||||
// only reset the view model if there is no FIDO reset in progress
|
||||
appViewModel.setDeviceInfo(null)
|
||||
fidoViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
override fun onPause() {
|
||||
// cancel any FIDO reset flow which might be in progress
|
||||
resetHelper.cancelReset()
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
deviceManager.addDeviceListener(this)
|
||||
|
||||
fidoChannel.setHandler(coroutineScope) { method, args ->
|
||||
when (method) {
|
||||
@ -171,12 +133,18 @@ class FidoManager(
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
if (!deviceManager.isUsbKeyConnected()) {
|
||||
// for NFC connections require extra tap when switching context
|
||||
if (fidoViewModel.sessionState.value == null) {
|
||||
fidoViewModel.setSessionState(Session.uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
super.dispose()
|
||||
deviceManager.removeDeviceListener(this)
|
||||
fidoChannel.setMethodCallHandler(null)
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
@ -207,7 +175,7 @@ class FidoManager(
|
||||
}
|
||||
|
||||
logger.debug("Setting device info: {}", deviceInfo)
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
deviceManager.setDeviceInfo(deviceInfo)
|
||||
}
|
||||
|
||||
// Clear any cached FIDO state
|
||||
@ -253,7 +221,7 @@ class FidoManager(
|
||||
// Update deviceInfo since the deviceId has changed
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val deviceInfo = DeviceUtil.readInfo(connection, pid)
|
||||
appViewModel.setDeviceInfo(
|
||||
deviceManager.setDeviceInfo(
|
||||
Info(
|
||||
name = DeviceUtil.getName(deviceInfo, pid?.type),
|
||||
isNfc = device.transport == Transport.NFC,
|
||||
@ -439,4 +407,14 @@ class FidoManager(
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
if (!resetHelper.inProgress) {
|
||||
fidoViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimeout() {
|
||||
fidoViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ package com.yubico.authenticator.fido
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.FidoResetState
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
@ -35,7 +36,7 @@ import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class FidoResetHelper(
|
||||
private val appViewModel: MainViewModel,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
private val connectionHelper: FidoConnectionHelper,
|
||||
private val pinStore: FidoPinStore
|
||||
@ -49,9 +50,10 @@ class FidoResetHelper(
|
||||
|
||||
suspend fun reset(): String {
|
||||
try {
|
||||
deviceManager.clearDeviceInfoOnDisconnect = false
|
||||
inProgress = true
|
||||
fidoViewModel.updateResetState(FidoResetState.Remove)
|
||||
val usb = appViewModel.connectedYubiKey.value != null
|
||||
val usb = deviceManager.isUsbKeyConnected()
|
||||
if (usb) {
|
||||
resetOverUSB()
|
||||
} else {
|
||||
@ -62,8 +64,8 @@ class FidoResetHelper(
|
||||
logger.debug("FIDO reset cancelled")
|
||||
} finally {
|
||||
inProgress = false
|
||||
if (appViewModel.connectedYubiKey.value == null) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
deviceManager.clearDeviceInfoOnDisconnect = true
|
||||
if (!deviceManager.isUsbKeyConnected()) {
|
||||
fidoViewModel.setSessionState(null)
|
||||
fidoViewModel.updateCredentials(emptyList())
|
||||
}
|
||||
@ -81,7 +83,7 @@ class FidoResetHelper(
|
||||
private suspend fun waitForUsbDisconnect() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch {
|
||||
cancelReset = false
|
||||
while (appViewModel.connectedYubiKey.value != null) {
|
||||
while (deviceManager.isUsbKeyConnected()) {
|
||||
if (cancelReset) {
|
||||
logger.debug("Reset was cancelled")
|
||||
continuation.resumeWithException(CancellationException())
|
||||
@ -98,7 +100,7 @@ class FidoResetHelper(
|
||||
coroutineScope.launch {
|
||||
fidoViewModel.updateResetState(FidoResetState.Insert)
|
||||
cancelReset = false
|
||||
while (appViewModel.connectedYubiKey.value == null) {
|
||||
while (!deviceManager.isUsbKeyConnected()) {
|
||||
if (cancelReset) {
|
||||
logger.debug("Reset was cancelled")
|
||||
continuation.resumeWithException(CancellationException())
|
||||
@ -111,45 +113,45 @@ class FidoResetHelper(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun resetAfterTouch() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
fidoViewModel.updateResetState(FidoResetState.Touch)
|
||||
logger.debug("Waiting for touch")
|
||||
connectionHelper.useSessionUsb(appViewModel.connectedYubiKey.value!!) { fidoSession ->
|
||||
resetCommandState = CommandState()
|
||||
try {
|
||||
doReset(fidoSession)
|
||||
continuation.resume(Unit)
|
||||
} catch (e: CtapException) {
|
||||
when (e.ctapError) {
|
||||
CtapException.ERR_KEEPALIVE_CANCEL -> {
|
||||
logger.debug("Received ERR_KEEPALIVE_CANCEL during FIDO reset")
|
||||
deviceManager.withKey { usbYubiKeyDevice ->
|
||||
connectionHelper.useSessionUsb(usbYubiKeyDevice) { fidoSession ->
|
||||
resetCommandState = CommandState()
|
||||
try {
|
||||
doReset(fidoSession)
|
||||
continuation.resume(Unit)
|
||||
} catch (e: CtapException) {
|
||||
when (e.ctapError) {
|
||||
CtapException.ERR_KEEPALIVE_CANCEL -> {
|
||||
logger.debug("Received ERR_KEEPALIVE_CANCEL during FIDO reset")
|
||||
}
|
||||
|
||||
CtapException.ERR_ACTION_TIMEOUT -> {
|
||||
logger.debug("Received ERR_ACTION_TIMEOUT during FIDO reset")
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.error("Received CtapException during FIDO reset: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
CtapException.ERR_ACTION_TIMEOUT -> {
|
||||
logger.debug("Received ERR_ACTION_TIMEOUT during FIDO reset")
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.error("Received CtapException during FIDO reset: ", e)
|
||||
}
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} catch (e: IOException) {
|
||||
// communication error, key was removed?
|
||||
logger.error("IOException during FIDO reset: ", e)
|
||||
// treat it as cancellation
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} finally {
|
||||
resetCommandState = null
|
||||
}
|
||||
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} catch (e: IOException) {
|
||||
// communication error, key was removed?
|
||||
logger.error("IOException during FIDO reset: ", e)
|
||||
// treat it as cancellation
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} finally {
|
||||
resetCommandState = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun resetOverUSB() {
|
||||
waitForUsbDisconnect()
|
||||
waitForConnection()
|
||||
@ -186,5 +188,4 @@ class FidoResetHelper(
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(FidoResetHelper::class.java)
|
||||
}
|
||||
|
||||
}
|
@ -24,7 +24,7 @@ import com.yubico.authenticator.fido.data.FidoResetState
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
|
||||
class FidoViewModel : ViewModel() {
|
||||
private val _sessionState = MutableLiveData<Session?>()
|
||||
private val _sessionState = MutableLiveData<Session?>(null)
|
||||
val sessionState: LiveData<Session?> = _sessionState
|
||||
|
||||
fun setSessionState(sessionState: Session?) {
|
||||
|
@ -87,10 +87,30 @@ data class SessionInfo(
|
||||
data class Session(
|
||||
@SerialName("info")
|
||||
val info: SessionInfo,
|
||||
@SerialName("unlocked")
|
||||
val unlocked: Boolean
|
||||
val unlocked: Boolean,
|
||||
val initialized: Boolean
|
||||
) {
|
||||
constructor(infoData: InfoData, unlocked: Boolean) : this(
|
||||
SessionInfo(infoData), unlocked
|
||||
SessionInfo(infoData), unlocked, true
|
||||
)
|
||||
|
||||
companion object {
|
||||
val uninitialized = Session(
|
||||
SessionInfo(
|
||||
Options(
|
||||
clientPin = false,
|
||||
credMgmt = false,
|
||||
credentialMgmtPreview = false,
|
||||
bioEnroll = null,
|
||||
alwaysUv = false
|
||||
),
|
||||
aaguid = ByteArray(0),
|
||||
minPinLength = 0,
|
||||
forcePinChange = false
|
||||
),
|
||||
unlocked = false,
|
||||
initialized = false
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -19,12 +19,13 @@ package com.yubico.authenticator.oath
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.*
|
||||
import com.yubico.authenticator.device.Capabilities
|
||||
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.oath.data.Code
|
||||
@ -73,13 +74,14 @@ typealias OathAction = (Result<YubiKitOathSession, Exception>) -> Unit
|
||||
class OathManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
messenger: BinaryMessenger,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val oathViewModel: OathViewModel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val appPreferences: AppPreferences,
|
||||
) : AppContextManager {
|
||||
) : AppContextManager(lifecycleOwner), DeviceListener {
|
||||
|
||||
companion object {
|
||||
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
|
||||
private val memoryKeyProvider = ClearingMemProvider()
|
||||
}
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
@ -87,7 +89,6 @@ class OathManager(
|
||||
|
||||
private val oathChannel = MethodChannel(messenger, "android.oath.methods")
|
||||
|
||||
private val memoryKeyProvider = ClearingMemProvider()
|
||||
private val keyManager by lazy {
|
||||
KeyManager(
|
||||
compatUtil.from(Build.VERSION_CODES.M) {
|
||||
@ -108,60 +109,23 @@ class OathManager(
|
||||
private var refreshJob: Job? = null
|
||||
private var addToAny = false
|
||||
|
||||
// provides actions for lifecycle events
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
|
||||
private var startTimeMs: Long = -1
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
startTimeMs = currentTimeMs
|
||||
|
||||
// cancel any pending actions, except for addToAny
|
||||
if (!addToAny) {
|
||||
pendingAction?.let {
|
||||
logger.debug("Cancelling pending action/closing nfc dialog.")
|
||||
it.invoke(Result.failure(CancellationException()))
|
||||
coroutineScope.launch {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
pendingAction = null
|
||||
override fun onPause() {
|
||||
// cancel any pending actions, except for addToAny
|
||||
if (!addToAny) {
|
||||
pendingAction?.let {
|
||||
logger.debug("Cancelling pending action/closing nfc dialog.")
|
||||
it.invoke(Result.failure(CancellationException()))
|
||||
coroutineScope.launch {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
pendingAction = null
|
||||
}
|
||||
|
||||
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)
|
||||
oathViewModel.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?> {
|
||||
refreshJob?.cancel()
|
||||
if (it == null) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val credentialObserver = Observer<List<CredentialWithCode>?> { codes ->
|
||||
refreshJob?.cancel()
|
||||
if (codes != null && appViewModel.connectedYubiKey.value != null) {
|
||||
if (codes != null && deviceManager.isUsbKeyConnected()) {
|
||||
val expirations = codes
|
||||
.filter { it.credential.codeType == CodeType.TOTP && !it.credential.touchRequired }
|
||||
.mapNotNull { it.code?.validTo }
|
||||
@ -190,7 +154,7 @@ class OathManager(
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
deviceManager.addDeviceListener(this)
|
||||
oathViewModel.credentials.observe(lifecycleOwner, credentialObserver)
|
||||
|
||||
// OATH methods callable from Flutter:
|
||||
@ -237,12 +201,17 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
if (!deviceManager.isUsbKeyConnected()) {
|
||||
// for NFC connections require extra tap when switching context
|
||||
if (oathViewModel.sessionState.value == null) {
|
||||
oathViewModel.setSessionState(Session.uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
super.dispose()
|
||||
deviceManager.removeDeviceListener(this)
|
||||
oathViewModel.credentials.removeObserver(credentialObserver)
|
||||
oathChannel.setMethodCallHandler(null)
|
||||
coroutineScope.cancel()
|
||||
@ -307,7 +276,7 @@ class OathManager(
|
||||
logger.error("Failed to recognize this OATH device.")
|
||||
// we know this is NFC device and it supports OATH
|
||||
val oathCapabilities = Capabilities(nfc = 0x20)
|
||||
appViewModel.setDeviceInfo(
|
||||
deviceManager.setDeviceInfo(
|
||||
UnknownDevice.copy(
|
||||
config = UnknownDevice.config.copy(enabledCapabilities = oathCapabilities),
|
||||
name = "Unknown OATH device",
|
||||
@ -322,7 +291,7 @@ class OathManager(
|
||||
// Update deviceInfo since the deviceId has changed
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val deviceInfo = DeviceUtil.readInfo(connection, pid)
|
||||
appViewModel.setDeviceInfo(
|
||||
deviceManager.setDeviceInfo(
|
||||
Info(
|
||||
name = DeviceUtil.getName(deviceInfo, pid?.type),
|
||||
isNfc = device.transport == Transport.NFC,
|
||||
@ -330,7 +299,6 @@ class OathManager(
|
||||
deviceInfo = deviceInfo
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
logger.debug(
|
||||
@ -351,7 +319,7 @@ class OathManager(
|
||||
}
|
||||
|
||||
logger.debug("Setting device info: {}", deviceInfo)
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
deviceManager.setDeviceInfo(deviceInfo)
|
||||
}
|
||||
|
||||
// Clear any cached OATH state
|
||||
@ -450,7 +418,7 @@ class OathManager(
|
||||
oathViewModel.setSessionState(Session(it, remembered))
|
||||
|
||||
// fetch credentials after unlocking only if the YubiKey is connected over USB
|
||||
if ( appViewModel.connectedYubiKey.value != null) {
|
||||
if (deviceManager.isUsbKeyConnected()) {
|
||||
oathViewModel.updateCredentials(calculateOathCodes(it))
|
||||
}
|
||||
}
|
||||
@ -495,7 +463,7 @@ class OathManager(
|
||||
throw Exception("Unset password failed")
|
||||
}
|
||||
|
||||
private suspend fun forgetPassword(): String {
|
||||
private fun forgetPassword(): String {
|
||||
keyManager.clearAll()
|
||||
logger.debug("Cleared all keys.")
|
||||
oathViewModel.sessionState.value?.let {
|
||||
@ -563,7 +531,7 @@ class OathManager(
|
||||
} ?: emptyMap())
|
||||
}
|
||||
|
||||
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
|
||||
deviceManager.withKey { usbYubiKeyDevice ->
|
||||
try {
|
||||
useOathSessionUsb(usbYubiKeyDevice) { session ->
|
||||
try {
|
||||
@ -688,7 +656,7 @@ class OathManager(
|
||||
|
||||
|
||||
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||
val isUsbKey = appViewModel.connectedYubiKey.value != null
|
||||
val isUsbKey = deviceManager.isUsbKeyConnected()
|
||||
var timestamp = System.currentTimeMillis()
|
||||
if (!isUsbKey) {
|
||||
// NFC, need to pad timer to avoid immediate expiration
|
||||
@ -717,9 +685,10 @@ class OathManager(
|
||||
|
||||
// callers can decide whether the session should be unlocked first
|
||||
unlockOnConnect.set(unlock)
|
||||
return appViewModel.connectedYubiKey.value?.let {
|
||||
useOathSessionUsb(it, action)
|
||||
} ?: useOathSessionNfc(oathActionDescription, action)
|
||||
return deviceManager.withKey(
|
||||
onUsb = { useOathSessionUsb(it, action) },
|
||||
onNfc = { useOathSessionNfc(oathActionDescription, action) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSessionUsb(
|
||||
@ -775,5 +744,16 @@ class OathManager(
|
||||
(credential != null) && credential.id.asString() == credentialId
|
||||
} ?: throw Exception("Failed to find account")
|
||||
|
||||
override fun onConnected(device: YubiKeyDevice) {
|
||||
refreshJob?.cancel()
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
refreshJob?.cancel()
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
|
||||
override fun onTimeout() {
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ data class Session(
|
||||
@SerialName("remembered")
|
||||
val isRemembered: Boolean,
|
||||
@SerialName("locked")
|
||||
val isLocked: Boolean
|
||||
val isLocked: Boolean,
|
||||
val initialized: Boolean
|
||||
) {
|
||||
@SerialName("keystore")
|
||||
@Suppress("unused")
|
||||
@ -50,6 +51,18 @@ data class Session(
|
||||
),
|
||||
oathSession.isAccessKeySet,
|
||||
isRemembered,
|
||||
oathSession.isLocked
|
||||
oathSession.isLocked,
|
||||
initialized = true
|
||||
)
|
||||
|
||||
companion object {
|
||||
val uninitialized = Session(
|
||||
deviceId = "",
|
||||
version = Version(0, 0, 0),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false,
|
||||
initialized = false
|
||||
)
|
||||
}
|
||||
}
|
@ -44,7 +44,8 @@ class ModelTest {
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false
|
||||
isLocked = false,
|
||||
initialized = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ class SerializationTest {
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false
|
||||
isLocked = false,
|
||||
initialized = true
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -70,8 +70,15 @@ Future<Widget> initialize() async {
|
||||
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
|
||||
credentialListProvider
|
||||
.overrideWithProvider(androidCredentialListProvider.call),
|
||||
currentAppProvider.overrideWith(
|
||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))),
|
||||
currentAppProvider.overrideWith((ref) {
|
||||
final notifier =
|
||||
AndroidSubPageNotifier(ref.watch(supportedAppsProvider));
|
||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider,
|
||||
(_, data) {
|
||||
notifier.notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
}),
|
||||
managementStateProvider.overrideWithProvider(androidManagementState.call),
|
||||
currentDeviceProvider.overrideWith(
|
||||
() => AndroidCurrentDeviceNotifier(),
|
||||
|
@ -104,6 +104,12 @@ class AndroidSubPageNotifier extends CurrentAppNotifier {
|
||||
void _handleSubPage(Application subPage) async {
|
||||
await _contextChannel.invokeMethod('setContext', {'index': subPage.index});
|
||||
}
|
||||
|
||||
@override
|
||||
void notifyDeviceChanged(YubiKeyData? data) {
|
||||
super.notifyDeviceChanged(data);
|
||||
_handleSubPage(state);
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier {
|
||||
|
@ -209,7 +209,7 @@ final currentAppProvider =
|
||||
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
|
||||
final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider));
|
||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
|
||||
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||
notifier.notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
@ -223,7 +223,7 @@ class CurrentAppNotifier extends StateNotifier<Application> {
|
||||
state = app;
|
||||
}
|
||||
|
||||
void _notifyDeviceChanged(YubiKeyData? data) {
|
||||
void notifyDeviceChanged(YubiKeyData? data) {
|
||||
if (data == null ||
|
||||
state.getAvailability(data) != Availability.unsupported) {
|
||||
// Keep current app
|
||||
|
@ -179,7 +179,7 @@ class _$SuccessImpl implements Success {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SuccessImpl &&
|
||||
@ -358,7 +358,7 @@ class _$SignalImpl implements Signal {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SignalImpl &&
|
||||
@ -547,7 +547,7 @@ class _$RpcErrorImpl implements RpcError {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$RpcErrorImpl &&
|
||||
@ -773,7 +773,7 @@ class _$RpcStateImpl implements _RpcState {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$RpcStateImpl &&
|
||||
|
@ -27,7 +27,8 @@ class FidoState with _$FidoState {
|
||||
|
||||
factory FidoState(
|
||||
{required Map<String, dynamic> info,
|
||||
required bool unlocked}) = _FidoState;
|
||||
required bool unlocked,
|
||||
@Default(true) bool initialized}) = _FidoState;
|
||||
|
||||
factory FidoState.fromJson(Map<String, dynamic> json) =>
|
||||
_$FidoStateFromJson(json);
|
||||
|
@ -22,6 +22,7 @@ FidoState _$FidoStateFromJson(Map<String, dynamic> json) {
|
||||
mixin _$FidoState {
|
||||
Map<String, dynamic> get info => throw _privateConstructorUsedError;
|
||||
bool get unlocked => throw _privateConstructorUsedError;
|
||||
bool get initialized => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
@ -34,7 +35,7 @@ abstract class $FidoStateCopyWith<$Res> {
|
||||
factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) =
|
||||
_$FidoStateCopyWithImpl<$Res, FidoState>;
|
||||
@useResult
|
||||
$Res call({Map<String, dynamic> info, bool unlocked});
|
||||
$Res call({Map<String, dynamic> info, bool unlocked, bool initialized});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -52,6 +53,7 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
|
||||
$Res call({
|
||||
Object? info = null,
|
||||
Object? unlocked = null,
|
||||
Object? initialized = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
info: null == info
|
||||
@ -62,6 +64,10 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
|
||||
? _value.unlocked
|
||||
: unlocked // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
initialized: null == initialized
|
||||
? _value.initialized
|
||||
: initialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
@ -74,7 +80,7 @@ abstract class _$$FidoStateImplCopyWith<$Res>
|
||||
__$$FidoStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Map<String, dynamic> info, bool unlocked});
|
||||
$Res call({Map<String, dynamic> info, bool unlocked, bool initialized});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -90,6 +96,7 @@ class __$$FidoStateImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? info = null,
|
||||
Object? unlocked = null,
|
||||
Object? initialized = null,
|
||||
}) {
|
||||
return _then(_$FidoStateImpl(
|
||||
info: null == info
|
||||
@ -100,6 +107,10 @@ class __$$FidoStateImplCopyWithImpl<$Res>
|
||||
? _value.unlocked
|
||||
: unlocked // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
initialized: null == initialized
|
||||
? _value.initialized
|
||||
: initialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -108,7 +119,9 @@ class __$$FidoStateImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$FidoStateImpl extends _FidoState {
|
||||
_$FidoStateImpl(
|
||||
{required final Map<String, dynamic> info, required this.unlocked})
|
||||
{required final Map<String, dynamic> info,
|
||||
required this.unlocked,
|
||||
this.initialized = true})
|
||||
: _info = info,
|
||||
super._();
|
||||
|
||||
@ -125,26 +138,31 @@ class _$FidoStateImpl extends _FidoState {
|
||||
|
||||
@override
|
||||
final bool unlocked;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool initialized;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FidoState(info: $info, unlocked: $unlocked)';
|
||||
return 'FidoState(info: $info, unlocked: $unlocked, initialized: $initialized)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FidoStateImpl &&
|
||||
const DeepCollectionEquality().equals(other._info, _info) &&
|
||||
(identical(other.unlocked, unlocked) ||
|
||||
other.unlocked == unlocked));
|
||||
other.unlocked == unlocked) &&
|
||||
(identical(other.initialized, initialized) ||
|
||||
other.initialized == initialized));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, const DeepCollectionEquality().hash(_info), unlocked);
|
||||
int get hashCode => Object.hash(runtimeType,
|
||||
const DeepCollectionEquality().hash(_info), unlocked, initialized);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -163,7 +181,8 @@ class _$FidoStateImpl extends _FidoState {
|
||||
abstract class _FidoState extends FidoState {
|
||||
factory _FidoState(
|
||||
{required final Map<String, dynamic> info,
|
||||
required final bool unlocked}) = _$FidoStateImpl;
|
||||
required final bool unlocked,
|
||||
final bool initialized}) = _$FidoStateImpl;
|
||||
_FidoState._() : super._();
|
||||
|
||||
factory _FidoState.fromJson(Map<String, dynamic> json) =
|
||||
@ -174,6 +193,8 @@ abstract class _FidoState extends FidoState {
|
||||
@override
|
||||
bool get unlocked;
|
||||
@override
|
||||
bool get initialized;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@ -265,7 +286,7 @@ class _$PinSuccessImpl implements _PinSuccess {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is _$PinSuccessImpl);
|
||||
}
|
||||
@ -392,7 +413,7 @@ class _$PinFailureImpl implements _PinFailure {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$PinFailureImpl &&
|
||||
@ -594,7 +615,7 @@ class _$FingerprintImpl extends _Fingerprint {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FingerprintImpl &&
|
||||
@ -750,7 +771,7 @@ class _$EventCaptureImpl implements _EventCapture {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$EventCaptureImpl &&
|
||||
@ -900,7 +921,7 @@ class _$EventCompleteImpl implements _EventComplete {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$EventCompleteImpl &&
|
||||
@ -1040,7 +1061,7 @@ class _$EventErrorImpl implements _EventError {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$EventErrorImpl &&
|
||||
@ -1274,7 +1295,7 @@ class _$FidoCredentialImpl implements _FidoCredential {
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FidoCredentialImpl &&
|
||||
|
@ -10,12 +10,14 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map<String, dynamic> json) =>
|
||||
_$FidoStateImpl(
|
||||
info: json['info'] as Map<String, dynamic>,
|
||||
unlocked: json['unlocked'] as bool,
|
||||
initialized: json['initialized'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'info': instance.info,
|
||||
'unlocked': instance.unlocked,
|
||||
'initialized': instance.initialized,
|
||||
};
|
||||
|
||||
_$FingerprintImpl _$$FingerprintImplFromJson(Map<String, dynamic> json) =>
|
||||
|
@ -43,6 +43,7 @@ import 'pin_entry_form.dart';
|
||||
|
||||
class PasskeysScreen extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
|
||||
const PasskeysScreen(this.deviceData, {super.key});
|
||||
|
||||
@override
|
||||
@ -73,13 +74,30 @@ class PasskeysScreen extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
data: (fidoState) {
|
||||
return fidoState.unlocked
|
||||
? _FidoUnlockedPage(deviceData.node, fidoState)
|
||||
: _FidoLockedPage(deviceData.node, fidoState);
|
||||
return fidoState.initialized
|
||||
? fidoState.unlocked
|
||||
? _FidoUnlockedPage(deviceData.node, fidoState)
|
||||
: _FidoLockedPage(deviceData.node, fidoState)
|
||||
: const _FidoInsertTapPage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FidoInsertTapPage extends ConsumerWidget {
|
||||
const _FidoInsertTapPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return MessagePage(
|
||||
title: l10n.s_passkeys,
|
||||
centered: false,
|
||||
capabilities: const [Capability.fido2],
|
||||
header: l10n.l_insert_or_tap_yk,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FidoLockedPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final FidoState state;
|
||||
|
@ -110,6 +110,7 @@ class OathState with _$OathState {
|
||||
required bool remembered,
|
||||
required bool locked,
|
||||
required KeystoreState keystore,
|
||||
@Default(true) bool initialized,
|
||||
}) = _OathState;
|
||||
|
||||
factory OathState.fromJson(Map<String, dynamic> json) =>
|
||||
|
@ -639,6 +639,7 @@ mixin _$OathState {
|
||||
bool get remembered => throw _privateConstructorUsedError;
|
||||
bool get locked => throw _privateConstructorUsedError;
|
||||
KeystoreState get keystore => throw _privateConstructorUsedError;
|
||||
bool get initialized => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
@ -657,7 +658,8 @@ abstract class $OathStateCopyWith<$Res> {
|
||||
bool hasKey,
|
||||
bool remembered,
|
||||
bool locked,
|
||||
KeystoreState keystore});
|
||||
KeystoreState keystore,
|
||||
bool initialized});
|
||||
|
||||
$VersionCopyWith<$Res> get version;
|
||||
}
|
||||
@ -681,6 +683,7 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState>
|
||||
Object? remembered = null,
|
||||
Object? locked = null,
|
||||
Object? keystore = null,
|
||||
Object? initialized = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
deviceId: null == deviceId
|
||||
@ -707,6 +710,10 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState>
|
||||
? _value.keystore
|
||||
: keystore // ignore: cast_nullable_to_non_nullable
|
||||
as KeystoreState,
|
||||
initialized: null == initialized
|
||||
? _value.initialized
|
||||
: initialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@ -733,7 +740,8 @@ abstract class _$$OathStateImplCopyWith<$Res>
|
||||
bool hasKey,
|
||||
bool remembered,
|
||||
bool locked,
|
||||
KeystoreState keystore});
|
||||
KeystoreState keystore,
|
||||
bool initialized});
|
||||
|
||||
@override
|
||||
$VersionCopyWith<$Res> get version;
|
||||
@ -756,6 +764,7 @@ class __$$OathStateImplCopyWithImpl<$Res>
|
||||
Object? remembered = null,
|
||||
Object? locked = null,
|
||||
Object? keystore = null,
|
||||
Object? initialized = null,
|
||||
}) {
|
||||
return _then(_$OathStateImpl(
|
||||
null == deviceId
|
||||
@ -782,6 +791,10 @@ class __$$OathStateImplCopyWithImpl<$Res>
|
||||
? _value.keystore
|
||||
: keystore // ignore: cast_nullable_to_non_nullable
|
||||
as KeystoreState,
|
||||
initialized: null == initialized
|
||||
? _value.initialized
|
||||
: initialized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -793,7 +806,8 @@ class _$OathStateImpl implements _OathState {
|
||||
{required this.hasKey,
|
||||
required this.remembered,
|
||||
required this.locked,
|
||||
required this.keystore});
|
||||
required this.keystore,
|
||||
this.initialized = true});
|
||||
|
||||
factory _$OathStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$OathStateImplFromJson(json);
|
||||
@ -810,10 +824,13 @@ class _$OathStateImpl implements _OathState {
|
||||
final bool locked;
|
||||
@override
|
||||
final KeystoreState keystore;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool initialized;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OathState(deviceId: $deviceId, version: $version, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore)';
|
||||
return 'OathState(deviceId: $deviceId, version: $version, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore, initialized: $initialized)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -829,13 +846,15 @@ class _$OathStateImpl implements _OathState {
|
||||
other.remembered == remembered) &&
|
||||
(identical(other.locked, locked) || other.locked == locked) &&
|
||||
(identical(other.keystore, keystore) ||
|
||||
other.keystore == keystore));
|
||||
other.keystore == keystore) &&
|
||||
(identical(other.initialized, initialized) ||
|
||||
other.initialized == initialized));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, deviceId, version, hasKey, remembered, locked, keystore);
|
||||
int get hashCode => Object.hash(runtimeType, deviceId, version, hasKey,
|
||||
remembered, locked, keystore, initialized);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -856,7 +875,8 @@ abstract class _OathState implements OathState {
|
||||
{required final bool hasKey,
|
||||
required final bool remembered,
|
||||
required final bool locked,
|
||||
required final KeystoreState keystore}) = _$OathStateImpl;
|
||||
required final KeystoreState keystore,
|
||||
final bool initialized}) = _$OathStateImpl;
|
||||
|
||||
factory _OathState.fromJson(Map<String, dynamic> json) =
|
||||
_$OathStateImpl.fromJson;
|
||||
@ -874,6 +894,8 @@ abstract class _OathState implements OathState {
|
||||
@override
|
||||
KeystoreState get keystore;
|
||||
@override
|
||||
bool get initialized;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$OathStateImplCopyWith<_$OathStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
@ -70,6 +70,7 @@ _$OathStateImpl _$$OathStateImplFromJson(Map<String, dynamic> json) =>
|
||||
remembered: json['remembered'] as bool,
|
||||
locked: json['locked'] as bool,
|
||||
keystore: $enumDecode(_$KeystoreStateEnumMap, json['keystore']),
|
||||
initialized: json['initialized'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
|
||||
@ -80,6 +81,7 @@ Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
|
||||
'remembered': instance.remembered,
|
||||
'locked': instance.locked,
|
||||
'keystore': _$KeystoreStateEnumMap[instance.keystore]!,
|
||||
'initialized': instance.initialized,
|
||||
};
|
||||
|
||||
const _$KeystoreStateEnumMap = {
|
||||
|
@ -65,13 +65,30 @@ class OathScreen extends ConsumerWidget {
|
||||
error: (error, _) => AppFailurePage(
|
||||
cause: error,
|
||||
),
|
||||
data: (oathState) => oathState.locked
|
||||
? _LockedView(devicePath, oathState)
|
||||
: _UnlockedView(devicePath, oathState),
|
||||
data: (oathState) => oathState.initialized
|
||||
? oathState.locked
|
||||
? _LockedView(devicePath, oathState)
|
||||
: _UnlockedView(devicePath, oathState)
|
||||
: const _InsertTapView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InsertTapView extends ConsumerWidget {
|
||||
const _InsertTapView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return MessagePage(
|
||||
title: AppLocalizations.of(context)!.s_accounts,
|
||||
centered: false,
|
||||
capabilities: const [Capability.oath],
|
||||
header: l10n.l_insert_or_tap_yk,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LockedView extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
final OathState oathState;
|
||||
|
@ -28,7 +28,6 @@ import '../state.dart';
|
||||
|
||||
class PinDialog extends ConsumerStatefulWidget {
|
||||
final DevicePath devicePath;
|
||||
|
||||
const PinDialog(this.devicePath, {super.key});
|
||||
|
||||
@override
|
||||
|
Loading…
Reference in New Issue
Block a user