mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
Merge PR #1686
This commit is contained in:
commit
7fa2786ccd
@ -22,9 +22,13 @@ import com.yubico.yubikit.core.YubiKeyDevice
|
|||||||
* Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app.
|
* Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app.
|
||||||
*/
|
*/
|
||||||
abstract class AppContextManager {
|
abstract class AppContextManager {
|
||||||
abstract suspend fun processYubiKey(device: YubiKeyDevice)
|
abstract suspend fun processYubiKey(device: YubiKeyDevice): Boolean
|
||||||
|
|
||||||
open fun dispose() {}
|
open fun dispose() {}
|
||||||
|
|
||||||
open fun onPause() {}
|
open fun onPause() {}
|
||||||
|
|
||||||
|
open fun onError() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ContextDisposedException : Exception()
|
@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022-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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.yubico.authenticator
|
|
||||||
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
typealias OnDialogCancelled = suspend () -> Unit
|
|
||||||
|
|
||||||
enum class DialogIcon(val value: Int) {
|
|
||||||
Nfc(0),
|
|
||||||
Success(1),
|
|
||||||
Failure(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class DialogTitle(val value: Int) {
|
|
||||||
TapKey(0),
|
|
||||||
OperationSuccessful(1),
|
|
||||||
OperationFailed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
|
|
||||||
private val channel =
|
|
||||||
MethodChannel(messenger, "com.yubico.authenticator.channel.dialog")
|
|
||||||
|
|
||||||
private var onCancelled: OnDialogCancelled? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
channel.setHandler(coroutineScope) { method, _ ->
|
|
||||||
when (method) {
|
|
||||||
"cancel" -> dialogClosed()
|
|
||||||
else -> throw NotImplementedError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showDialog(
|
|
||||||
dialogIcon: DialogIcon,
|
|
||||||
dialogTitle: DialogTitle,
|
|
||||||
dialogDescriptionId: Int,
|
|
||||||
cancelled: OnDialogCancelled?
|
|
||||||
) {
|
|
||||||
onCancelled = cancelled
|
|
||||||
coroutineScope.launch {
|
|
||||||
channel.invoke(
|
|
||||||
"show",
|
|
||||||
Json.encodeToString(
|
|
||||||
mapOf(
|
|
||||||
"title" to dialogTitle.value,
|
|
||||||
"description" to dialogDescriptionId,
|
|
||||||
"icon" to dialogIcon.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateDialogState(
|
|
||||||
dialogIcon: DialogIcon? = null,
|
|
||||||
dialogTitle: DialogTitle,
|
|
||||||
dialogDescriptionId: Int? = null,
|
|
||||||
) {
|
|
||||||
channel.invoke(
|
|
||||||
"state",
|
|
||||||
Json.encodeToString(
|
|
||||||
mapOf(
|
|
||||||
"title" to dialogTitle.value,
|
|
||||||
"description" to dialogDescriptionId,
|
|
||||||
"icon" to dialogIcon?.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun closeDialog() {
|
|
||||||
channel.invoke("close", NULL)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun dialogClosed(): String {
|
|
||||||
onCancelled?.let {
|
|
||||||
onCancelled = null
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
it.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,8 +16,11 @@
|
|||||||
|
|
||||||
package com.yubico.authenticator
|
package com.yubico.authenticator
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.*
|
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@ -48,13 +51,18 @@ import com.yubico.authenticator.management.ManagementHandler
|
|||||||
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
||||||
import com.yubico.authenticator.oath.OathManager
|
import com.yubico.authenticator.oath.OathManager
|
||||||
import com.yubico.authenticator.oath.OathViewModel
|
import com.yubico.authenticator.oath.OathViewModel
|
||||||
|
import com.yubico.authenticator.yubikit.NfcStateDispatcher
|
||||||
|
import com.yubico.authenticator.yubikit.NfcStateListener
|
||||||
|
import com.yubico.authenticator.yubikit.NfcState
|
||||||
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
|
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
|
||||||
import com.yubico.authenticator.yubikit.withConnection
|
import com.yubico.authenticator.yubikit.withConnection
|
||||||
import com.yubico.yubikit.android.YubiKitManager
|
import com.yubico.yubikit.android.YubiKitManager
|
||||||
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||||
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||||
|
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyManager
|
||||||
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
||||||
|
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager
|
||||||
import com.yubico.yubikit.core.Transport
|
import com.yubico.yubikit.core.Transport
|
||||||
import com.yubico.yubikit.core.YubiKeyDevice
|
import com.yubico.yubikit.core.YubiKeyDevice
|
||||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||||
@ -66,6 +74,7 @@ import io.flutter.embedding.android.FlutterFragmentActivity
|
|||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -94,6 +103,20 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MainActivity::class.java)
|
private val logger = LoggerFactory.getLogger(MainActivity::class.java)
|
||||||
|
|
||||||
|
private val nfcStateListener = object : NfcStateListener {
|
||||||
|
|
||||||
|
var appMethodChannel : AppMethodChannel? = null
|
||||||
|
|
||||||
|
override fun onChange(newState: NfcState) {
|
||||||
|
appMethodChannel?.let {
|
||||||
|
logger.debug("set nfc state to ${newState.name}")
|
||||||
|
it.nfcStateChanged(newState)
|
||||||
|
} ?: {
|
||||||
|
logger.warn("failed set nfc state to ${newState.name} - no method channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -105,7 +128,10 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
allowScreenshots(false)
|
allowScreenshots(false)
|
||||||
|
|
||||||
yubikit = YubiKitManager(this)
|
yubikit = YubiKitManager(
|
||||||
|
UsbYubiKeyManager(this),
|
||||||
|
NfcYubiKeyManager(this, NfcStateDispatcher(nfcStateListener))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
@ -291,10 +317,15 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (device is NfcYubiKeyDevice) {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.ONGOING)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceManager.scpKeyParams = null
|
||||||
// If NFC and FIPS check for SCP11b key
|
// If NFC and FIPS check for SCP11b key
|
||||||
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
||||||
logger.debug("Checking for usable SCP11b key...")
|
logger.debug("Checking for usable SCP11b key...")
|
||||||
deviceManager.scpKeyParams =
|
deviceManager.scpKeyParams = try {
|
||||||
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
||||||
val scp = SecurityDomainSession(connection)
|
val scp = SecurityDomainSession(connection)
|
||||||
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
||||||
@ -308,6 +339,14 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
logger.debug("Found SCP11b key: {}", keyRef)
|
logger.debug("Found SCP11b key: {}", keyRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.debug("Exception while getting scp keys: ", e)
|
||||||
|
contextManager?.onError()
|
||||||
|
if (device is NfcYubiKeyDevice) {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC
|
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC
|
||||||
@ -319,6 +358,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
deviceManager.setDeviceInfo(deviceInfo)
|
deviceManager.setDeviceInfo(deviceInfo)
|
||||||
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
|
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
|
||||||
logger.debug("Connected key supports: {}", supportedContexts)
|
logger.debug("Connected key supports: {}", supportedContexts)
|
||||||
|
var switchedContext: Boolean = false
|
||||||
if (!supportedContexts.contains(viewModel.appContext.value)) {
|
if (!supportedContexts.contains(viewModel.appContext.value)) {
|
||||||
val preferredContext = DeviceManager.getPreferredContext(supportedContexts)
|
val preferredContext = DeviceManager.getPreferredContext(supportedContexts)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -326,18 +366,28 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
viewModel.appContext.value,
|
viewModel.appContext.value,
|
||||||
preferredContext
|
preferredContext
|
||||||
)
|
)
|
||||||
switchContext(preferredContext)
|
switchedContext = switchContext(preferredContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextManager == null && supportedContexts.isNotEmpty()) {
|
if (contextManager == null && supportedContexts.isNotEmpty()) {
|
||||||
switchContext(DeviceManager.getPreferredContext(supportedContexts))
|
switchedContext = switchContext(DeviceManager.getPreferredContext(supportedContexts))
|
||||||
}
|
}
|
||||||
|
|
||||||
contextManager?.let {
|
contextManager?.let {
|
||||||
try {
|
try {
|
||||||
it.processYubiKey(device)
|
val requestHandled = it.processYubiKey(device)
|
||||||
} catch (e: Throwable) {
|
if (requestHandled) {
|
||||||
logger.error("Error processing YubiKey in AppContextManager", e)
|
appMethodChannel.nfcStateChanged(NfcState.SUCCESS)
|
||||||
|
}
|
||||||
|
if (!switchedContext && device is NfcYubiKeyDevice) {
|
||||||
|
|
||||||
|
device.remove {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.IDLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.debug("Caught Exception during YubiKey processing: ", e)
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,7 +401,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
private var contextManager: AppContextManager? = null
|
private var contextManager: AppContextManager? = null
|
||||||
private lateinit var deviceManager: DeviceManager
|
private lateinit var deviceManager: DeviceManager
|
||||||
private lateinit var appContext: AppContext
|
private lateinit var appContext: AppContext
|
||||||
private lateinit var dialogManager: DialogManager
|
private lateinit var nfcOverlayManager: NfcOverlayManager
|
||||||
private lateinit var appPreferences: AppPreferences
|
private lateinit var appPreferences: AppPreferences
|
||||||
private lateinit var flutterLog: FlutterLog
|
private lateinit var flutterLog: FlutterLog
|
||||||
private lateinit var flutterStreams: List<Closeable>
|
private lateinit var flutterStreams: List<Closeable>
|
||||||
@ -365,13 +415,16 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
messenger = flutterEngine.dartExecutor.binaryMessenger
|
messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||||
flutterLog = FlutterLog(messenger)
|
flutterLog = FlutterLog(messenger)
|
||||||
deviceManager = DeviceManager(this, viewModel)
|
|
||||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
|
||||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
|
||||||
appPreferences = AppPreferences(this)
|
|
||||||
appMethodChannel = AppMethodChannel(messenger)
|
appMethodChannel = AppMethodChannel(messenger)
|
||||||
|
nfcOverlayManager = NfcOverlayManager(messenger, this.lifecycleScope)
|
||||||
|
deviceManager = DeviceManager(this, viewModel,appMethodChannel, nfcOverlayManager)
|
||||||
|
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||||
|
|
||||||
|
appPreferences = AppPreferences(this)
|
||||||
appLinkMethodChannel = AppLinkMethodChannel(messenger)
|
appLinkMethodChannel = AppLinkMethodChannel(messenger)
|
||||||
managementHandler = ManagementHandler(messenger, deviceManager, dialogManager)
|
managementHandler = ManagementHandler(messenger, deviceManager)
|
||||||
|
|
||||||
|
nfcStateListener.appMethodChannel = appMethodChannel
|
||||||
|
|
||||||
flutterStreams = listOf(
|
flutterStreams = listOf(
|
||||||
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
|
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
|
||||||
@ -390,7 +443,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchContext(appContext: OperationContext) {
|
private fun switchContext(appContext: OperationContext) : Boolean {
|
||||||
|
var switchHappened = false
|
||||||
// TODO: refactor this when more OperationContext are handled
|
// TODO: refactor this when more OperationContext are handled
|
||||||
// only recreate the contextManager object if it cannot be reused
|
// only recreate the contextManager object if it cannot be reused
|
||||||
if (appContext == OperationContext.Home ||
|
if (appContext == OperationContext.Home ||
|
||||||
@ -404,6 +458,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
contextManager?.dispose()
|
contextManager?.dispose()
|
||||||
contextManager = null
|
contextManager = null
|
||||||
|
switchHappened = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextManager == null) {
|
if (contextManager == null) {
|
||||||
@ -413,7 +468,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
messenger,
|
messenger,
|
||||||
deviceManager,
|
deviceManager,
|
||||||
oathViewModel,
|
oathViewModel,
|
||||||
dialogManager,
|
nfcOverlayManager,
|
||||||
appPreferences
|
appPreferences
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -422,17 +477,20 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
messenger,
|
messenger,
|
||||||
this,
|
this,
|
||||||
deviceManager,
|
deviceManager,
|
||||||
|
appMethodChannel,
|
||||||
|
nfcOverlayManager,
|
||||||
fidoViewModel,
|
fidoViewModel,
|
||||||
viewModel,
|
viewModel
|
||||||
dialogManager
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return switchHappened
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
nfcStateListener.appMethodChannel = null
|
||||||
flutterStreams.forEach { it.close() }
|
flutterStreams.forEach { it.close() }
|
||||||
contextManager?.dispose()
|
contextManager?.dispose()
|
||||||
deviceManager.dispose()
|
deviceManager.dispose()
|
||||||
@ -572,9 +630,18 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
fun nfcAdapterStateChanged(value: Boolean) {
|
fun nfcAdapterStateChanged(value: Boolean) {
|
||||||
methodChannel.invokeMethod(
|
methodChannel.invokeMethod(
|
||||||
"nfcAdapterStateChanged",
|
"nfcAdapterStateChanged",
|
||||||
JSONObject(mapOf("nfcEnabled" to value)).toString()
|
JSONObject(mapOf("enabled" to value)).toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun nfcStateChanged(activityState: NfcState) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
methodChannel.invokeMethod(
|
||||||
|
"nfcStateChanged",
|
||||||
|
JSONObject(mapOf("state" to activityState.value)).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allowScreenshots(value: Boolean): Boolean {
|
private fun allowScreenshots(value: Boolean): Boolean {
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022-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
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
typealias OnCancelled = suspend () -> Unit
|
||||||
|
|
||||||
|
class NfcOverlayManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
|
||||||
|
private val channel =
|
||||||
|
MethodChannel(messenger, "com.yubico.authenticator.channel.nfc_overlay")
|
||||||
|
|
||||||
|
private var onCancelled: OnCancelled? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
channel.setHandler(coroutineScope) { method, _ ->
|
||||||
|
when (method) {
|
||||||
|
"cancel" -> onClosed()
|
||||||
|
else -> throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(cancelled: OnCancelled?) {
|
||||||
|
onCancelled = cancelled
|
||||||
|
coroutineScope.launch {
|
||||||
|
channel.invoke("show", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun close() {
|
||||||
|
channel.invoke("close", NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onClosed(): String {
|
||||||
|
onCancelled?.let {
|
||||||
|
onCancelled = null
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL
|
||||||
|
}
|
||||||
|
}
|
@ -20,13 +20,19 @@ import androidx.collection.ArraySet
|
|||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import com.yubico.authenticator.ContextDisposedException
|
||||||
|
import com.yubico.authenticator.MainActivity
|
||||||
import com.yubico.authenticator.MainViewModel
|
import com.yubico.authenticator.MainViewModel
|
||||||
|
import com.yubico.authenticator.NfcOverlayManager
|
||||||
import com.yubico.authenticator.OperationContext
|
import com.yubico.authenticator.OperationContext
|
||||||
|
import com.yubico.authenticator.yubikit.NfcState
|
||||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||||
import com.yubico.yubikit.core.YubiKeyDevice
|
import com.yubico.yubikit.core.YubiKeyDevice
|
||||||
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
||||||
import com.yubico.yubikit.management.Capability
|
import com.yubico.yubikit.management.Capability
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
interface DeviceListener {
|
interface DeviceListener {
|
||||||
// a USB device is connected
|
// a USB device is connected
|
||||||
@ -41,7 +47,9 @@ interface DeviceListener {
|
|||||||
|
|
||||||
class DeviceManager(
|
class DeviceManager(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val appViewModel: MainViewModel
|
private val appViewModel: MainViewModel,
|
||||||
|
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||||
|
private val nfcOverlayManager: NfcOverlayManager
|
||||||
) {
|
) {
|
||||||
var clearDeviceInfoOnDisconnect: Boolean = true
|
var clearDeviceInfoOnDisconnect: Boolean = true
|
||||||
|
|
||||||
@ -167,7 +175,6 @@ class DeviceManager(
|
|||||||
|
|
||||||
fun setDeviceInfo(deviceInfo: Info?) {
|
fun setDeviceInfo(deviceInfo: Info?) {
|
||||||
appViewModel.setDeviceInfo(deviceInfo)
|
appViewModel.setDeviceInfo(deviceInfo)
|
||||||
scpKeyParams = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUsbKeyConnected(): Boolean {
|
fun isUsbKeyConnected(): Boolean {
|
||||||
@ -179,8 +186,32 @@ class DeviceManager(
|
|||||||
onUsb(it)
|
onUsb(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
suspend fun <T> withKey(
|
||||||
|
onUsb: suspend (UsbYubiKeyDevice) -> T,
|
||||||
|
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
|
||||||
|
onCancelled: () -> Unit
|
||||||
|
): T =
|
||||||
appViewModel.connectedYubiKey.value?.let {
|
appViewModel.connectedYubiKey.value?.let {
|
||||||
onUsb(it)
|
onUsb(it)
|
||||||
} ?: onNfc()
|
} ?: onNfc(onNfc, onCancelled)
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun <T> onNfc(
|
||||||
|
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
|
||||||
|
onCancelled: () -> Unit
|
||||||
|
): T {
|
||||||
|
nfcOverlayManager.show {
|
||||||
|
logger.debug("NFC action was cancelled")
|
||||||
|
onCancelled.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return onNfc.invoke().value.also {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.SUCCESS)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,9 +16,6 @@
|
|||||||
|
|
||||||
package com.yubico.authenticator.fido
|
package com.yubico.authenticator.fido
|
||||||
|
|
||||||
import com.yubico.authenticator.DialogIcon
|
|
||||||
import com.yubico.authenticator.DialogManager
|
|
||||||
import com.yubico.authenticator.DialogTitle
|
|
||||||
import com.yubico.authenticator.device.DeviceManager
|
import com.yubico.authenticator.device.DeviceManager
|
||||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||||
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
|
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
|
||||||
@ -27,18 +24,28 @@ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
|||||||
import com.yubico.yubikit.core.fido.FidoConnection
|
import com.yubico.yubikit.core.fido.FidoConnection
|
||||||
import com.yubico.yubikit.core.util.Result
|
import com.yubico.yubikit.core.util.Result
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.TimerTask
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class FidoConnectionHelper(
|
class FidoConnectionHelper(private val deviceManager: DeviceManager) {
|
||||||
private val deviceManager: DeviceManager,
|
|
||||||
private val dialogManager: DialogManager
|
|
||||||
) {
|
|
||||||
private var pendingAction: FidoAction? = null
|
private var pendingAction: FidoAction? = null
|
||||||
|
|
||||||
fun invokePending(fidoSession: YubiKitFidoSession) {
|
fun invokePending(fidoSession: YubiKitFidoSession): Boolean {
|
||||||
|
var requestHandled = true
|
||||||
pendingAction?.let { action ->
|
pendingAction?.let { action ->
|
||||||
|
pendingAction = null
|
||||||
|
// it is the pending action who handles this request
|
||||||
|
requestHandled = false
|
||||||
action.invoke(Result.success(fidoSession))
|
action.invoke(Result.success(fidoSession))
|
||||||
|
}
|
||||||
|
return requestHandled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun failPending(e: Exception) {
|
||||||
|
pendingAction?.let { action ->
|
||||||
|
logger.error("Failing pending action with {}", e.message)
|
||||||
|
action.invoke(Result.failure(e))
|
||||||
pendingAction = null
|
pendingAction = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,14 +58,18 @@ class FidoConnectionHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> useSession(
|
suspend fun <T> useSession(
|
||||||
actionDescription: FidoActionDescription,
|
|
||||||
updateDeviceInfo: Boolean = false,
|
updateDeviceInfo: Boolean = false,
|
||||||
action: (YubiKitFidoSession) -> T
|
block: (YubiKitFidoSession) -> T
|
||||||
): T {
|
): T {
|
||||||
FidoManager.updateDeviceInfo.set(updateDeviceInfo)
|
FidoManager.updateDeviceInfo.set(updateDeviceInfo)
|
||||||
return deviceManager.withKey(
|
return deviceManager.withKey(
|
||||||
onNfc = { useSessionNfc(actionDescription,action) },
|
onUsb = { useSessionUsb(it, updateDeviceInfo, block) },
|
||||||
onUsb = { useSessionUsb(it, updateDeviceInfo, action) })
|
onNfc = { useSessionNfc(block) },
|
||||||
|
onCancelled = {
|
||||||
|
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||||
|
pendingAction = null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> useSessionUsb(
|
suspend fun <T> useSessionUsb(
|
||||||
@ -74,9 +85,8 @@ class FidoConnectionHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> useSessionNfc(
|
suspend fun <T> useSessionNfc(
|
||||||
actionDescription: FidoActionDescription,
|
|
||||||
block: (YubiKitFidoSession) -> T
|
block: (YubiKitFidoSession) -> T
|
||||||
): T {
|
): Result<T, Throwable> {
|
||||||
try {
|
try {
|
||||||
val result = suspendCoroutine { outer ->
|
val result = suspendCoroutine { outer ->
|
||||||
pendingAction = {
|
pendingAction = {
|
||||||
@ -84,23 +94,13 @@ class FidoConnectionHelper(
|
|||||||
block.invoke(it.value)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
return Result.success(result!!)
|
||||||
} catch (cancelled: CancellationException) {
|
} catch (cancelled: CancellationException) {
|
||||||
throw cancelled
|
return Result.failure(cancelled)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
throw error
|
logger.error("Exception during action: ", error)
|
||||||
} finally {
|
return Result.failure(error)
|
||||||
dialogManager.closeDialog()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ package com.yubico.authenticator.fido
|
|||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.yubico.authenticator.AppContextManager
|
import com.yubico.authenticator.AppContextManager
|
||||||
import com.yubico.authenticator.DialogManager
|
import com.yubico.authenticator.NfcOverlayManager
|
||||||
|
import com.yubico.authenticator.MainActivity
|
||||||
import com.yubico.authenticator.MainViewModel
|
import com.yubico.authenticator.MainViewModel
|
||||||
import com.yubico.authenticator.NULL
|
import com.yubico.authenticator.NULL
|
||||||
import com.yubico.authenticator.asString
|
import com.yubico.authenticator.asString
|
||||||
@ -70,9 +71,10 @@ class FidoManager(
|
|||||||
messenger: BinaryMessenger,
|
messenger: BinaryMessenger,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
private val deviceManager: DeviceManager,
|
private val deviceManager: DeviceManager,
|
||||||
|
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||||
|
private val nfcOverlayManager: NfcOverlayManager,
|
||||||
private val fidoViewModel: FidoViewModel,
|
private val fidoViewModel: FidoViewModel,
|
||||||
mainViewModel: MainViewModel,
|
mainViewModel: MainViewModel
|
||||||
dialogManager: DialogManager,
|
|
||||||
) : AppContextManager(), DeviceListener {
|
) : AppContextManager(), DeviceListener {
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@ -100,7 +102,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectionHelper = FidoConnectionHelper(deviceManager, dialogManager)
|
private val connectionHelper = FidoConnectionHelper(deviceManager)
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||||
@ -117,14 +119,14 @@ class FidoManager(
|
|||||||
FidoResetHelper(
|
FidoResetHelper(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
deviceManager,
|
deviceManager,
|
||||||
|
appMethodChannel,
|
||||||
|
nfcOverlayManager,
|
||||||
fidoViewModel,
|
fidoViewModel,
|
||||||
mainViewModel,
|
mainViewModel,
|
||||||
connectionHelper,
|
connectionHelper,
|
||||||
pinStore
|
pinStore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pinRetries = null
|
pinRetries = null
|
||||||
|
|
||||||
@ -172,6 +174,12 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onError() {
|
||||||
|
super.onError()
|
||||||
|
logger.debug("Cancel any pending action because of upstream error")
|
||||||
|
connectionHelper.cancelPending()
|
||||||
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
super.dispose()
|
super.dispose()
|
||||||
deviceManager.removeDeviceListener(this)
|
deviceManager.removeDeviceListener(this)
|
||||||
@ -182,15 +190,16 @@ class FidoManager(
|
|||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
override suspend fun processYubiKey(device: YubiKeyDevice): Boolean {
|
||||||
|
var requestHandled = true
|
||||||
try {
|
try {
|
||||||
if (device.supportsConnection(FidoConnection::class.java)) {
|
if (device.supportsConnection(FidoConnection::class.java)) {
|
||||||
device.withConnection<FidoConnection, Unit> { connection ->
|
device.withConnection<FidoConnection, Unit> { connection ->
|
||||||
processYubiKey(connection, device)
|
requestHandled = processYubiKey(connection, device)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||||
processYubiKey(connection, device)
|
requestHandled = processYubiKey(connection, device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,13 +210,21 @@ class FidoManager(
|
|||||||
// something went wrong, try to get DeviceInfo from any available connection type
|
// something went wrong, try to get DeviceInfo from any available connection type
|
||||||
logger.error("Failure when processing YubiKey: ", e)
|
logger.error("Failure when processing YubiKey: ", e)
|
||||||
|
|
||||||
// Clear any cached FIDO state
|
connectionHelper.failPending(e)
|
||||||
fidoViewModel.clearSessionState()
|
|
||||||
|
if (e !is IOException) {
|
||||||
|
// we don't clear the session on IOExceptions so that the session is ready for
|
||||||
|
// a possible re-run of a failed action.
|
||||||
|
fidoViewModel.clearSessionState()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return requestHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice) {
|
private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice): Boolean {
|
||||||
|
var requestHandled = true
|
||||||
val fidoSession =
|
val fidoSession =
|
||||||
if (connection is FidoConnection) {
|
if (connection is FidoConnection) {
|
||||||
YubiKitFidoSession(connection)
|
YubiKitFidoSession(connection)
|
||||||
@ -226,7 +243,7 @@ class FidoManager(
|
|||||||
val sameDevice = currentSession == previousSession
|
val sameDevice = currentSession == previousSession
|
||||||
|
|
||||||
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
|
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
|
||||||
connectionHelper.invokePending(fidoSession)
|
requestHandled = connectionHelper.invokePending(fidoSession)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (!sameDevice) {
|
if (!sameDevice) {
|
||||||
@ -250,6 +267,8 @@ class FidoManager(
|
|||||||
Session(infoData, pinStore.hasPin(), pinRetries)
|
Session(infoData, pinStore.hasPin(), pinRetries)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return requestHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int {
|
private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int {
|
||||||
@ -353,7 +372,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun unlock(pin: CharArray): String =
|
private suspend fun unlock(pin: CharArray): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.Unlock) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val clientPin =
|
val clientPin =
|
||||||
@ -390,7 +409,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
|
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.SetPin, updateDeviceInfo = true) { fidoSession ->
|
connectionHelper.useSession(updateDeviceInfo = true) { fidoSession ->
|
||||||
try {
|
try {
|
||||||
val clientPin =
|
val clientPin =
|
||||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
@ -438,7 +457,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deleteCredential(rpId: String, credentialId: String): String =
|
private suspend fun deleteCredential(rpId: String, credentialId: String): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.DeleteCredential) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
|
|
||||||
val clientPin =
|
val clientPin =
|
||||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
@ -486,7 +505,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deleteFingerprint(templateId: String): String =
|
private suspend fun deleteFingerprint(templateId: String): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
|
|
||||||
val clientPin =
|
val clientPin =
|
||||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
@ -511,7 +530,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun renameFingerprint(templateId: String, name: String): String =
|
private suspend fun renameFingerprint(templateId: String, name: String): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
|
|
||||||
val clientPin =
|
val clientPin =
|
||||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
@ -541,7 +560,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerFingerprint(name: String?): String =
|
private suspend fun registerFingerprint(name: String?): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
state?.cancel()
|
state?.cancel()
|
||||||
state = CommandState()
|
state = CommandState()
|
||||||
val clientPin =
|
val clientPin =
|
||||||
@ -588,7 +607,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
else -> throw ctapException
|
else -> throw ctapException
|
||||||
}
|
}
|
||||||
} catch (io: IOException) {
|
} catch (_: IOException) {
|
||||||
return@useSession JSONObject(
|
return@useSession JSONObject(
|
||||||
mapOf(
|
mapOf(
|
||||||
"success" to false,
|
"success" to false,
|
||||||
@ -617,7 +636,7 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun enableEnterpriseAttestation(): String =
|
private suspend fun enableEnterpriseAttestation(): String =
|
||||||
connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession ->
|
connectionHelper.useSession { fidoSession ->
|
||||||
try {
|
try {
|
||||||
val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)
|
val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)
|
||||||
val clientPin = ClientPin(fidoSession, uvAuthProtocol)
|
val clientPin = ClientPin(fidoSession, uvAuthProtocol)
|
||||||
|
@ -18,11 +18,14 @@ package com.yubico.authenticator.fido
|
|||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.yubico.authenticator.NfcOverlayManager
|
||||||
|
import com.yubico.authenticator.MainActivity
|
||||||
import com.yubico.authenticator.MainViewModel
|
import com.yubico.authenticator.MainViewModel
|
||||||
import com.yubico.authenticator.NULL
|
import com.yubico.authenticator.NULL
|
||||||
import com.yubico.authenticator.device.DeviceManager
|
import com.yubico.authenticator.device.DeviceManager
|
||||||
import com.yubico.authenticator.fido.data.Session
|
import com.yubico.authenticator.fido.data.Session
|
||||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||||
|
import com.yubico.authenticator.yubikit.NfcState
|
||||||
import com.yubico.yubikit.core.application.CommandState
|
import com.yubico.yubikit.core.application.CommandState
|
||||||
import com.yubico.yubikit.core.fido.CtapException
|
import com.yubico.yubikit.core.fido.CtapException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -68,6 +71,8 @@ fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent {
|
|||||||
class FidoResetHelper(
|
class FidoResetHelper(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val deviceManager: DeviceManager,
|
private val deviceManager: DeviceManager,
|
||||||
|
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||||
|
private val nfcOverlayManager: NfcOverlayManager,
|
||||||
private val fidoViewModel: FidoViewModel,
|
private val fidoViewModel: FidoViewModel,
|
||||||
private val mainViewModel: MainViewModel,
|
private val mainViewModel: MainViewModel,
|
||||||
private val connectionHelper: FidoConnectionHelper,
|
private val connectionHelper: FidoConnectionHelper,
|
||||||
@ -106,7 +111,7 @@ class FidoResetHelper(
|
|||||||
resetOverNfc()
|
resetOverNfc()
|
||||||
}
|
}
|
||||||
logger.info("FIDO reset complete")
|
logger.info("FIDO reset complete")
|
||||||
} catch (e: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
logger.debug("FIDO reset cancelled")
|
logger.debug("FIDO reset cancelled")
|
||||||
} finally {
|
} finally {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@ -210,16 +215,22 @@ class FidoResetHelper(
|
|||||||
|
|
||||||
private suspend fun resetOverNfc() = suspendCoroutine { continuation ->
|
private suspend fun resetOverNfc() = suspendCoroutine { continuation ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
nfcOverlayManager.show {
|
||||||
|
|
||||||
|
}
|
||||||
fidoViewModel.updateResetState(FidoResetState.Touch)
|
fidoViewModel.updateResetState(FidoResetState.Touch)
|
||||||
try {
|
try {
|
||||||
FidoManager.updateDeviceInfo.set(true)
|
FidoManager.updateDeviceInfo.set(true)
|
||||||
connectionHelper.useSessionNfc(FidoActionDescription.Reset) { fidoSession ->
|
connectionHelper.useSessionNfc { fidoSession ->
|
||||||
doReset(fidoSession)
|
doReset(fidoSession)
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.SUCCESS)
|
||||||
continuation.resume(Unit)
|
continuation.resume(Unit)
|
||||||
}
|
}.value
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// on NFC, clean device info in this situation
|
// on NFC, clean device info in this situation
|
||||||
mainViewModel.setDeviceInfo(null)
|
mainViewModel.setDeviceInfo(null)
|
||||||
|
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
|
||||||
|
logger.error("Failure during FIDO reset:", e)
|
||||||
continuation.resumeWithException(e)
|
continuation.resumeWithException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,11 @@
|
|||||||
|
|
||||||
package com.yubico.authenticator.management
|
package com.yubico.authenticator.management
|
||||||
|
|
||||||
import com.yubico.authenticator.DialogIcon
|
|
||||||
import com.yubico.authenticator.DialogManager
|
|
||||||
import com.yubico.authenticator.DialogTitle
|
|
||||||
import com.yubico.authenticator.device.DeviceManager
|
import com.yubico.authenticator.device.DeviceManager
|
||||||
import com.yubico.authenticator.yubikit.withConnection
|
import com.yubico.authenticator.yubikit.withConnection
|
||||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||||
import com.yubico.yubikit.core.util.Result
|
import com.yubico.yubikit.core.util.Result
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@ -32,19 +28,19 @@ typealias YubiKitManagementSession = com.yubico.yubikit.management.ManagementSes
|
|||||||
typealias ManagementAction = (Result<YubiKitManagementSession, Exception>) -> Unit
|
typealias ManagementAction = (Result<YubiKitManagementSession, Exception>) -> Unit
|
||||||
|
|
||||||
class ManagementConnectionHelper(
|
class ManagementConnectionHelper(
|
||||||
private val deviceManager: DeviceManager,
|
private val deviceManager: DeviceManager
|
||||||
private val dialogManager: DialogManager
|
|
||||||
) {
|
) {
|
||||||
private var action: ManagementAction? = null
|
private var action: ManagementAction? = null
|
||||||
|
|
||||||
suspend fun <T> useSession(
|
suspend fun <T> useSession(block: (YubiKitManagementSession) -> T): T =
|
||||||
actionDescription: ManagementActionDescription,
|
deviceManager.withKey(
|
||||||
action: (YubiKitManagementSession) -> T
|
onUsb = { useSessionUsb(it, block) },
|
||||||
): T {
|
onNfc = { useSessionNfc(block) },
|
||||||
return deviceManager.withKey(
|
onCancelled = {
|
||||||
onNfc = { useSessionNfc(actionDescription, action) },
|
action?.invoke(Result.failure(CancellationException()))
|
||||||
onUsb = { useSessionUsb(it, action) })
|
action = null
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun <T> useSessionUsb(
|
private suspend fun <T> useSessionUsb(
|
||||||
device: UsbYubiKeyDevice,
|
device: UsbYubiKeyDevice,
|
||||||
@ -54,37 +50,20 @@ class ManagementConnectionHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> useSessionNfc(
|
private suspend fun <T> useSessionNfc(
|
||||||
actionDescription: ManagementActionDescription,
|
block: (YubiKitManagementSession) -> T): Result<T, Throwable> {
|
||||||
block: (YubiKitManagementSession) -> T
|
|
||||||
): T {
|
|
||||||
try {
|
try {
|
||||||
val result = suspendCoroutine { outer ->
|
val result = suspendCoroutine<T> { outer ->
|
||||||
action = {
|
action = {
|
||||||
outer.resumeWith(runCatching {
|
outer.resumeWith(runCatching {
|
||||||
block.invoke(it.value)
|
block.invoke(it.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
dialogManager.showDialog(
|
|
||||||
DialogIcon.Nfc,
|
|
||||||
DialogTitle.TapKey,
|
|
||||||
actionDescription.id
|
|
||||||
) {
|
|
||||||
logger.debug("Cancelled Dialog {}", actionDescription.name)
|
|
||||||
action?.invoke(Result.failure(CancellationException()))
|
|
||||||
action = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
return Result.success(result!!)
|
||||||
} catch (cancelled: CancellationException) {
|
} catch (cancelled: CancellationException) {
|
||||||
throw cancelled
|
return Result.failure(cancelled)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
throw error
|
return Result.failure(error)
|
||||||
} finally {
|
|
||||||
dialogManager.closeDialog()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val logger = LoggerFactory.getLogger(ManagementConnectionHelper::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package com.yubico.authenticator.management
|
package com.yubico.authenticator.management
|
||||||
|
|
||||||
import com.yubico.authenticator.DialogManager
|
|
||||||
import com.yubico.authenticator.NULL
|
import com.yubico.authenticator.NULL
|
||||||
import com.yubico.authenticator.device.DeviceManager
|
import com.yubico.authenticator.device.DeviceManager
|
||||||
import com.yubico.authenticator.setHandler
|
import com.yubico.authenticator.setHandler
|
||||||
@ -27,25 +26,15 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
const val dialogDescriptionManagementIndex = 300
|
|
||||||
|
|
||||||
enum class ManagementActionDescription(private val value: Int) {
|
|
||||||
DeviceReset(0), ActionFailure(1);
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
get() = value + dialogDescriptionManagementIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManagementHandler(
|
class ManagementHandler(
|
||||||
messenger: BinaryMessenger,
|
messenger: BinaryMessenger,
|
||||||
deviceManager: DeviceManager,
|
deviceManager: DeviceManager
|
||||||
dialogManager: DialogManager
|
|
||||||
) {
|
) {
|
||||||
private val channel = MethodChannel(messenger, "android.management.methods")
|
private val channel = MethodChannel(messenger, "android.management.methods")
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||||
private val connectionHelper = ManagementConnectionHelper(deviceManager, dialogManager)
|
private val connectionHelper = ManagementConnectionHelper(deviceManager)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
channel.setHandler(coroutineScope) { method, _ ->
|
channel.setHandler(coroutineScope) { method, _ ->
|
||||||
@ -58,7 +47,7 @@ class ManagementHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deviceReset(): String =
|
private suspend fun deviceReset(): String =
|
||||||
connectionHelper.useSession(ManagementActionDescription.DeviceReset) { managementSession ->
|
connectionHelper.useSession { managementSession ->
|
||||||
managementSession.deviceReset()
|
managementSession.deviceReset()
|
||||||
NULL
|
NULL
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.util.TimerTask
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
@ -74,8 +75,8 @@ class OathManager(
|
|||||||
messenger: BinaryMessenger,
|
messenger: BinaryMessenger,
|
||||||
private val deviceManager: DeviceManager,
|
private val deviceManager: DeviceManager,
|
||||||
private val oathViewModel: OathViewModel,
|
private val oathViewModel: OathViewModel,
|
||||||
private val dialogManager: DialogManager,
|
private val nfcOverlayManager: NfcOverlayManager,
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences
|
||||||
) : AppContextManager(), DeviceListener {
|
) : AppContextManager(), DeviceListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -107,15 +108,26 @@ class OathManager(
|
|||||||
private var refreshJob: Job? = null
|
private var refreshJob: Job? = null
|
||||||
private var addToAny = false
|
private var addToAny = false
|
||||||
private val updateDeviceInfo = AtomicBoolean(false)
|
private val updateDeviceInfo = AtomicBoolean(false)
|
||||||
|
private var deviceInfoTimer: TimerTask? = null
|
||||||
|
|
||||||
|
override fun onError() {
|
||||||
|
super.onError()
|
||||||
|
logger.debug("Cancel any pending action because of upstream error")
|
||||||
|
pendingAction?.let { action ->
|
||||||
|
action.invoke(Result.failure(CancellationException()))
|
||||||
|
pendingAction = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
deviceInfoTimer?.cancel()
|
||||||
// cancel any pending actions, except for addToAny
|
// cancel any pending actions, except for addToAny
|
||||||
if (!addToAny) {
|
if (!addToAny) {
|
||||||
pendingAction?.let {
|
pendingAction?.let {
|
||||||
logger.debug("Cancelling pending action/closing nfc dialog.")
|
logger.debug("Cancelling pending action/closing nfc overlay.")
|
||||||
it.invoke(Result.failure(CancellationException()))
|
it.invoke(Result.failure(CancellationException()))
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
dialogManager.closeDialog()
|
nfcOverlayManager.close()
|
||||||
}
|
}
|
||||||
pendingAction = null
|
pendingAction = null
|
||||||
}
|
}
|
||||||
@ -186,6 +198,7 @@ class OathManager(
|
|||||||
)
|
)
|
||||||
|
|
||||||
"deleteAccount" -> deleteAccount(args["credentialId"] as String)
|
"deleteAccount" -> deleteAccount(args["credentialId"] as String)
|
||||||
|
|
||||||
"addAccountToAny" -> addAccountToAny(
|
"addAccountToAny" -> addAccountToAny(
|
||||||
args["uri"] as String,
|
args["uri"] as String,
|
||||||
args["requireTouch"] as Boolean
|
args["requireTouch"] as Boolean
|
||||||
@ -208,28 +221,59 @@ class OathManager(
|
|||||||
oathChannel.setMethodCallHandler(null)
|
oathChannel.setMethodCallHandler(null)
|
||||||
oathViewModel.clearSession()
|
oathViewModel.clearSession()
|
||||||
oathViewModel.updateCredentials(mapOf())
|
oathViewModel.updateCredentials(mapOf())
|
||||||
pendingAction?.invoke(Result.failure(Exception()))
|
pendingAction?.invoke(Result.failure(ContextDisposedException()))
|
||||||
|
pendingAction = null
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
override suspend fun processYubiKey(device: YubiKeyDevice): Boolean {
|
||||||
|
var requestHandled = true
|
||||||
try {
|
try {
|
||||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||||
val session = getOathSession(connection)
|
val session = getOathSession(connection)
|
||||||
val previousId = oathViewModel.currentSession()?.deviceId
|
val previousId = oathViewModel.currentSession()?.deviceId
|
||||||
if (session.deviceId == previousId && device is NfcYubiKeyDevice) {
|
// only run pending action over NFC
|
||||||
// Run any pending action
|
// when the device is still the same
|
||||||
pendingAction?.let { action ->
|
// or when there is no previous device, but we have a pending action
|
||||||
action.invoke(Result.success(session))
|
if (device is NfcYubiKeyDevice &&
|
||||||
pendingAction = null
|
((session.deviceId == previousId) ||
|
||||||
|
(previousId == null && pendingAction != null))
|
||||||
|
) {
|
||||||
|
// update session if it is null
|
||||||
|
if (previousId == null) {
|
||||||
|
oathViewModel.setSessionState(
|
||||||
|
Session(
|
||||||
|
session,
|
||||||
|
keyManager.isRemembered(session.deviceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!session.isLocked) {
|
||||||
|
try {
|
||||||
|
// only load the accounts without calculating the codes
|
||||||
|
oathViewModel.updateCredentials(getAccounts(session))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
oathViewModel.updateCredentials(emptyMap())
|
||||||
|
} }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh codes
|
// Either run a pending action, or just refresh codes
|
||||||
if (!session.isLocked) {
|
if (pendingAction != null) {
|
||||||
try {
|
pendingAction?.let { action ->
|
||||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
pendingAction = null
|
||||||
} catch (error: Exception) {
|
// it is the pending action who handles this request
|
||||||
logger.error("Failed to refresh codes", error)
|
requestHandled = false
|
||||||
|
action.invoke(Result.success(session))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Refresh codes
|
||||||
|
if (!session.isLocked) {
|
||||||
|
try {
|
||||||
|
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||||
|
} catch (error: Exception) {
|
||||||
|
logger.error("Failed to refresh codes: ", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -246,7 +290,15 @@ class OathManager(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (!session.isLocked) {
|
if (!session.isLocked) {
|
||||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
try {
|
||||||
|
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// in this situation we clear the session because otherwise
|
||||||
|
// the credential list would be in loading state
|
||||||
|
// clearing the session will prompt the user to try again
|
||||||
|
oathViewModel.clearSession()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Awaiting an action for a different or no device?
|
// Awaiting an action for a different or no device?
|
||||||
@ -255,6 +307,7 @@ class OathManager(
|
|||||||
if (addToAny) {
|
if (addToAny) {
|
||||||
// Special "add to any YubiKey" action, process
|
// Special "add to any YubiKey" action, process
|
||||||
addToAny = false
|
addToAny = false
|
||||||
|
requestHandled = false
|
||||||
action.invoke(Result.success(session))
|
action.invoke(Result.success(session))
|
||||||
} else {
|
} else {
|
||||||
// Awaiting an action for a different device? Fail it and stop processing.
|
// Awaiting an action for a different device? Fail it and stop processing.
|
||||||
@ -284,6 +337,7 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Successfully read Oath session info (and credentials if unlocked) from connected key"
|
"Successfully read Oath session info (and credentials if unlocked) from connected key"
|
||||||
)
|
)
|
||||||
@ -293,11 +347,25 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
||||||
logger.error("Failed to connect to CCID: ", e)
|
logger.error("Exception during SmartCard connection/OATH session creation: ", e)
|
||||||
|
|
||||||
// Clear any cached OATH state
|
// Remove any pending action
|
||||||
oathViewModel.clearSession()
|
pendingAction?.let { action ->
|
||||||
|
logger.error("Failing pending action with {}", e.message)
|
||||||
|
action.invoke(Result.failure(e))
|
||||||
|
pendingAction = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e !is IOException) {
|
||||||
|
// we don't clear the session on IOExceptions so that the session is ready for
|
||||||
|
// a possible re-run of a failed action.
|
||||||
|
oathViewModel.clearSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return requestHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addAccountToAny(
|
private suspend fun addAccountToAny(
|
||||||
@ -307,7 +375,7 @@ class OathManager(
|
|||||||
val credentialData: CredentialData =
|
val credentialData: CredentialData =
|
||||||
CredentialData.parseUri(URI.create(uri))
|
CredentialData.parseUri(URI.create(uri))
|
||||||
addToAny = true
|
addToAny = true
|
||||||
return useOathSessionNfc(OathActionDescription.AddAccount) { session ->
|
return useOathSession { session ->
|
||||||
// We need to check for duplicates here since we haven't yet read the credentials
|
// We need to check for duplicates here since we haven't yet read the credentials
|
||||||
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
|
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
@ -337,7 +405,7 @@ class OathManager(
|
|||||||
logger.trace("Adding following accounts: {}", uris)
|
logger.trace("Adding following accounts: {}", uris)
|
||||||
|
|
||||||
addToAny = true
|
addToAny = true
|
||||||
return useOathSession(OathActionDescription.AddMultipleAccounts) { session ->
|
return useOathSession { session ->
|
||||||
var successCount = 0
|
var successCount = 0
|
||||||
for (index in uris.indices) {
|
for (index in uris.indices) {
|
||||||
|
|
||||||
@ -369,7 +437,7 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun reset(): String =
|
private suspend fun reset(): String =
|
||||||
useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) {
|
useOathSession(updateDeviceInfo = true) {
|
||||||
// note, it is ok to reset locked session
|
// note, it is ok to reset locked session
|
||||||
it.reset()
|
it.reset()
|
||||||
keyManager.removeKey(it.deviceId)
|
keyManager.removeKey(it.deviceId)
|
||||||
@ -381,7 +449,7 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun unlock(password: String, remember: Boolean): String =
|
private suspend fun unlock(password: String, remember: Boolean): String =
|
||||||
useOathSession(OathActionDescription.Unlock) {
|
useOathSession {
|
||||||
val accessKey = it.deriveAccessKey(password.toCharArray())
|
val accessKey = it.deriveAccessKey(password.toCharArray())
|
||||||
keyManager.addKey(it.deviceId, accessKey, remember)
|
keyManager.addKey(it.deviceId, accessKey, remember)
|
||||||
|
|
||||||
@ -390,9 +458,13 @@ class OathManager(
|
|||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
oathViewModel.setSessionState(Session(it, remembered))
|
oathViewModel.setSessionState(Session(it, remembered))
|
||||||
|
|
||||||
// fetch credentials after unlocking only if the YubiKey is connected over USB
|
try {
|
||||||
if (deviceManager.isUsbKeyConnected()) {
|
|
||||||
oathViewModel.updateCredentials(calculateOathCodes(it))
|
oathViewModel.updateCredentials(calculateOathCodes(it))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// after unlocking there was problem getting the codes
|
||||||
|
// to avoid inconsistent UI, clear the session
|
||||||
|
oathViewModel.clearSession()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +476,6 @@ class OathManager(
|
|||||||
newPassword: String,
|
newPassword: String,
|
||||||
): String =
|
): String =
|
||||||
useOathSession(
|
useOathSession(
|
||||||
OathActionDescription.SetPassword,
|
|
||||||
unlock = false,
|
unlock = false,
|
||||||
updateDeviceInfo = true
|
updateDeviceInfo = true
|
||||||
) { session ->
|
) { session ->
|
||||||
@ -426,7 +497,7 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun unsetPassword(currentPassword: String): String =
|
private suspend fun unsetPassword(currentPassword: String): String =
|
||||||
useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session ->
|
useOathSession(unlock = false) { session ->
|
||||||
if (session.isAccessKeySet) {
|
if (session.isAccessKeySet) {
|
||||||
// test current password sent by the user
|
// test current password sent by the user
|
||||||
if (session.unlock(currentPassword.toCharArray())) {
|
if (session.unlock(currentPassword.toCharArray())) {
|
||||||
@ -458,7 +529,7 @@ class OathManager(
|
|||||||
uri: String,
|
uri: String,
|
||||||
requireTouch: Boolean,
|
requireTouch: Boolean,
|
||||||
): String =
|
): String =
|
||||||
useOathSession(OathActionDescription.AddAccount) { session ->
|
useOathSession { session ->
|
||||||
val credentialData: CredentialData =
|
val credentialData: CredentialData =
|
||||||
CredentialData.parseUri(URI.create(uri))
|
CredentialData.parseUri(URI.create(uri))
|
||||||
|
|
||||||
@ -479,21 +550,24 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun renameAccount(uri: String, name: String, issuer: String?): String =
|
private suspend fun renameAccount(uri: String, name: String, issuer: String?): String =
|
||||||
useOathSession(OathActionDescription.RenameAccount) { session ->
|
useOathSession { session ->
|
||||||
val credential = getOathCredential(session, uri)
|
val credential = getCredential(uri)
|
||||||
val renamedCredential =
|
val renamed = Credential(
|
||||||
Credential(session.renameCredential(credential, name, issuer), session.deviceId)
|
session.renameCredential(credential, name, issuer),
|
||||||
oathViewModel.renameCredential(
|
session.deviceId
|
||||||
Credential(credential, session.deviceId),
|
|
||||||
renamedCredential
|
|
||||||
)
|
)
|
||||||
|
|
||||||
jsonSerializer.encodeToString(renamedCredential)
|
oathViewModel.renameCredential(
|
||||||
|
Credential(credential, session.deviceId),
|
||||||
|
renamed
|
||||||
|
)
|
||||||
|
|
||||||
|
jsonSerializer.encodeToString(renamed)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deleteAccount(credentialId: String): String =
|
private suspend fun deleteAccount(credentialId: String): String =
|
||||||
useOathSession(OathActionDescription.DeleteAccount) { session ->
|
useOathSession { session ->
|
||||||
val credential = getOathCredential(session, credentialId)
|
val credential = getCredential(credentialId)
|
||||||
session.deleteCredential(credential)
|
session.deleteCredential(credential)
|
||||||
oathViewModel.removeCredential(Credential(credential, session.deviceId))
|
oathViewModel.removeCredential(Credential(credential, session.deviceId))
|
||||||
NULL
|
NULL
|
||||||
@ -510,7 +584,7 @@ class OathManager(
|
|||||||
|
|
||||||
deviceManager.withKey { usbYubiKeyDevice ->
|
deviceManager.withKey { usbYubiKeyDevice ->
|
||||||
try {
|
try {
|
||||||
useOathSessionUsb(usbYubiKeyDevice) { session ->
|
useSessionUsb(usbYubiKeyDevice) { session ->
|
||||||
try {
|
try {
|
||||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||||
} catch (apduException: ApduException) {
|
} catch (apduException: ApduException) {
|
||||||
@ -534,7 +608,10 @@ class OathManager(
|
|||||||
logger.error("IOException when accessing USB device: ", ioException)
|
logger.error("IOException when accessing USB device: ", ioException)
|
||||||
clearCodes()
|
clearCodes()
|
||||||
} catch (illegalStateException: IllegalStateException) {
|
} catch (illegalStateException: IllegalStateException) {
|
||||||
logger.error("IllegalStateException when accessing USB device: ", illegalStateException)
|
logger.error(
|
||||||
|
"IllegalStateException when accessing USB device: ",
|
||||||
|
illegalStateException
|
||||||
|
)
|
||||||
clearCodes()
|
clearCodes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -542,8 +619,8 @@ class OathManager(
|
|||||||
|
|
||||||
|
|
||||||
private suspend fun calculate(credentialId: String): String =
|
private suspend fun calculate(credentialId: String): String =
|
||||||
useOathSession(OathActionDescription.CalculateCode) { session ->
|
useOathSession { session ->
|
||||||
val credential = getOathCredential(session, credentialId)
|
val credential = getCredential(credentialId)
|
||||||
|
|
||||||
val code = Code.from(calculateCode(session, credential))
|
val code = Code.from(calculateCode(session, credential))
|
||||||
oathViewModel.updateCode(
|
oathViewModel.updateCode(
|
||||||
@ -633,6 +710,14 @@ class OathManager(
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAccounts(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||||
|
return session.credentials.map { credential ->
|
||||||
|
Pair(
|
||||||
|
Credential(credential, session.deviceId),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||||
val isUsbKey = deviceManager.isUsbKeyConnected()
|
val isUsbKey = deviceManager.isUsbKeyConnected()
|
||||||
@ -645,35 +730,51 @@ class OathManager(
|
|||||||
return session.calculateCodes(timestamp).map { (credential, code) ->
|
return session.calculateCodes(timestamp).map { (credential, code) ->
|
||||||
Pair(
|
Pair(
|
||||||
Credential(credential, session.deviceId),
|
Credential(credential, session.deviceId),
|
||||||
Code.from(if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) {
|
Code.from(
|
||||||
session.calculateSteamCode(credential, timestamp)
|
if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) {
|
||||||
} else if (credential.isTouchRequired && bypassTouch) {
|
session.calculateSteamCode(credential, timestamp)
|
||||||
session.calculateCode(credential, timestamp)
|
} else if (credential.isTouchRequired && bypassTouch) {
|
||||||
} else {
|
session.calculateCode(credential, timestamp)
|
||||||
code
|
} else {
|
||||||
})
|
code
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCredential(id: String): YubiKitCredential {
|
||||||
|
val credential =
|
||||||
|
oathViewModel.credentials.value?.find { it.credential.id == id }?.credential
|
||||||
|
|
||||||
|
if (credential == null || credential.data == null) {
|
||||||
|
logger.debug("Failed to find credential with id: {}", id)
|
||||||
|
throw Exception("Failed to find account")
|
||||||
|
}
|
||||||
|
|
||||||
|
return credential.data
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <T> useOathSession(
|
private suspend fun <T> useOathSession(
|
||||||
oathActionDescription: OathActionDescription,
|
|
||||||
unlock: Boolean = true,
|
unlock: Boolean = true,
|
||||||
updateDeviceInfo: Boolean = false,
|
updateDeviceInfo: Boolean = false,
|
||||||
action: (YubiKitOathSession) -> T
|
block: (YubiKitOathSession) -> T
|
||||||
): T {
|
): T {
|
||||||
|
|
||||||
// callers can decide whether the session should be unlocked first
|
// callers can decide whether the session should be unlocked first
|
||||||
unlockOnConnect.set(unlock)
|
unlockOnConnect.set(unlock)
|
||||||
// callers can request whether device info should be updated after session operation
|
// callers can request whether device info should be updated after session operation
|
||||||
this@OathManager.updateDeviceInfo.set(updateDeviceInfo)
|
this@OathManager.updateDeviceInfo.set(updateDeviceInfo)
|
||||||
return deviceManager.withKey(
|
return deviceManager.withKey(
|
||||||
onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) },
|
onUsb = { useSessionUsb(it, updateDeviceInfo, block) },
|
||||||
onNfc = { useOathSessionNfc(oathActionDescription, action) }
|
onNfc = { useSessionNfc(block) },
|
||||||
|
onCancelled = {
|
||||||
|
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||||
|
pendingAction = null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> useOathSessionUsb(
|
private suspend fun <T> useSessionUsb(
|
||||||
device: UsbYubiKeyDevice,
|
device: UsbYubiKeyDevice,
|
||||||
updateDeviceInfo: Boolean = false,
|
updateDeviceInfo: Boolean = false,
|
||||||
block: (YubiKitOathSession) -> T
|
block: (YubiKitOathSession) -> T
|
||||||
@ -685,10 +786,9 @@ class OathManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> useOathSessionNfc(
|
private suspend fun <T> useSessionNfc(
|
||||||
oathActionDescription: OathActionDescription,
|
block: (YubiKitOathSession) -> T,
|
||||||
block: (YubiKitOathSession) -> T
|
): Result<T, Throwable> {
|
||||||
): T {
|
|
||||||
try {
|
try {
|
||||||
val result = suspendCoroutine { outer ->
|
val result = suspendCoroutine { outer ->
|
||||||
pendingAction = {
|
pendingAction = {
|
||||||
@ -696,41 +796,18 @@ class OathManager(
|
|||||||
block.invoke(it.value)
|
block.invoke(it.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) {
|
// here the coroutine is suspended and waits till pendingAction is
|
||||||
logger.debug("Cancelled Dialog {}", oathActionDescription.name)
|
// invoked - the pending action result will resume this coroutine
|
||||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
|
||||||
pendingAction = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dialogManager.updateDialogState(
|
return Result.success(result!!)
|
||||||
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) {
|
} catch (cancelled: CancellationException) {
|
||||||
throw cancelled
|
return Result.failure(cancelled)
|
||||||
} catch (error: Throwable) {
|
} catch (e: Exception) {
|
||||||
dialogManager.updateDialogState(
|
logger.error("Exception during action: ", e)
|
||||||
dialogIcon = DialogIcon.Failure,
|
return Result.failure(e)
|
||||||
dialogTitle = DialogTitle.OperationFailed,
|
|
||||||
dialogDescriptionId = OathActionDescription.ActionFailure.id
|
|
||||||
)
|
|
||||||
// TODO: This delays the closing of the dialog, but also the return value
|
|
||||||
delay(1500)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
dialogManager.closeDialog()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOathCredential(session: YubiKitOathSession, credentialId: String) =
|
|
||||||
// we need to use oathSession.calculateCodes() to get proper Credential.touchRequired value
|
|
||||||
session.calculateCodes().map { e -> e.key }.firstOrNull { credential ->
|
|
||||||
(credential != null) && credential.id.asString() == credentialId
|
|
||||||
} ?: throw Exception("Failed to find account")
|
|
||||||
|
|
||||||
override fun onConnected(device: YubiKeyDevice) {
|
override fun onConnected(device: YubiKeyDevice) {
|
||||||
refreshJob?.cancel()
|
refreshJob?.cancel()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 Yubico.
|
* Copyright (C) 2023-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -35,9 +35,10 @@ data class Credential(
|
|||||||
@SerialName("name")
|
@SerialName("name")
|
||||||
val accountName: String,
|
val accountName: String,
|
||||||
@SerialName("touch_required")
|
@SerialName("touch_required")
|
||||||
val touchRequired: Boolean
|
val touchRequired: Boolean,
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
val data: YubiKitCredential? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(credential: YubiKitCredential, deviceId: String) : this(
|
constructor(credential: YubiKitCredential, deviceId: String) : this(
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
id = credential.id.asString(),
|
id = credential.id.asString(),
|
||||||
@ -48,7 +49,8 @@ data class Credential(
|
|||||||
period = credential.period,
|
period = credential.period,
|
||||||
issuer = credential.issuer,
|
issuer = credential.issuer,
|
||||||
accountName = credential.accountName,
|
accountName = credential.accountName,
|
||||||
touchRequired = credential.isTouchRequired
|
touchRequired = credential.isTouchRequired,
|
||||||
|
data = credential
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean =
|
override fun equals(other: Any?): Boolean =
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 Yubico.
|
* Copyright (C) 2023-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,22 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.yubico.authenticator.oath
|
package com.yubico.authenticator.yubikit
|
||||||
|
|
||||||
const val dialogDescriptionOathIndex = 100
|
enum class NfcState(val value: Int) {
|
||||||
|
DISABLED(0),
|
||||||
enum class OathActionDescription(private val value: Int) {
|
IDLE(1),
|
||||||
Reset(0),
|
ONGOING(2),
|
||||||
Unlock(1),
|
SUCCESS(3),
|
||||||
SetPassword(2),
|
FAILURE(4)
|
||||||
UnsetPassword(3),
|
|
||||||
AddAccount(4),
|
|
||||||
RenameAccount(5),
|
|
||||||
DeleteAccount(6),
|
|
||||||
CalculateCode(7),
|
|
||||||
ActionFailure(8),
|
|
||||||
AddMultipleAccounts(9);
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
get() = value + dialogDescriptionOathIndex
|
|
||||||
}
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023-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.yubikit
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.nfc.NfcAdapter
|
||||||
|
|
||||||
|
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||||
|
import com.yubico.yubikit.android.transport.nfc.NfcDispatcher
|
||||||
|
import com.yubico.yubikit.android.transport.nfc.NfcReaderDispatcher
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
interface NfcStateListener {
|
||||||
|
fun onChange(newState: NfcState)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcStateDispatcher(private val listener: NfcStateListener) : NfcDispatcher {
|
||||||
|
|
||||||
|
private lateinit var adapter: NfcAdapter
|
||||||
|
private lateinit var yubikitNfcDispatcher: NfcReaderDispatcher
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(NfcStateDispatcher::class.java)
|
||||||
|
|
||||||
|
override fun enable(
|
||||||
|
activity: Activity,
|
||||||
|
nfcConfiguration: NfcConfiguration,
|
||||||
|
handler: NfcDispatcher.OnTagHandler
|
||||||
|
) {
|
||||||
|
adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||||
|
yubikitNfcDispatcher = NfcReaderDispatcher(adapter)
|
||||||
|
|
||||||
|
logger.debug("enabling yubikit NFC state dispatcher")
|
||||||
|
yubikitNfcDispatcher.enable(
|
||||||
|
activity,
|
||||||
|
nfcConfiguration,
|
||||||
|
handler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disable(activity: Activity) {
|
||||||
|
listener.onChange(NfcState.DISABLED)
|
||||||
|
yubikitNfcDispatcher.disable(activity)
|
||||||
|
logger.debug("disabling yubikit NFC state dispatcher")
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
|
|
||||||
@ -73,8 +74,14 @@ void setupAppMethodsChannel(WidgetRef ref) {
|
|||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case 'nfcAdapterStateChanged':
|
case 'nfcAdapterStateChanged':
|
||||||
{
|
{
|
||||||
var nfcEnabled = args['nfcEnabled'];
|
var enabled = args['enabled'];
|
||||||
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(nfcEnabled);
|
ref.read(androidNfcAdapterState.notifier).enable(enabled);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'nfcStateChanged':
|
||||||
|
{
|
||||||
|
var nfcState = args['state'];
|
||||||
|
ref.read(androidNfcState.notifier).set(nfcState);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -32,17 +32,18 @@ import '../../exception/no_data_exception.dart';
|
|||||||
import '../../exception/platform_exception_decoder.dart';
|
import '../../exception/platform_exception_decoder.dart';
|
||||||
import '../../fido/models.dart';
|
import '../../fido/models.dart';
|
||||||
import '../../fido/state.dart';
|
import '../../fido/state.dart';
|
||||||
|
import '../overlay/nfc/method_channel_notifier.dart';
|
||||||
|
|
||||||
final _log = Logger('android.fido.state');
|
final _log = Logger('android.fido.state');
|
||||||
|
|
||||||
const _methods = MethodChannel('android.fido.methods');
|
|
||||||
|
|
||||||
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
|
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
|
||||||
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
|
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
|
||||||
|
|
||||||
class _FidoStateNotifier extends FidoStateNotifier {
|
class _FidoStateNotifier extends FidoStateNotifier {
|
||||||
final _events = const EventChannel('android.fido.sessionState');
|
final _events = const EventChannel('android.fido.sessionState');
|
||||||
late StreamSubscription _sub;
|
late StreamSubscription _sub;
|
||||||
|
late final _FidoMethodChannelNotifier fido =
|
||||||
|
ref.read(_fidoMethodsProvider.notifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<FidoState> build(DevicePath devicePath) async {
|
FutureOr<FidoState> build(DevicePath devicePath) async {
|
||||||
@ -79,7 +80,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
controller.onCancel = () async {
|
controller.onCancel = () async {
|
||||||
await _methods.invokeMethod('cancelReset');
|
await fido.invoke('cancelReset');
|
||||||
if (!controller.isClosed) {
|
if (!controller.isClosed) {
|
||||||
await subscription.cancel();
|
await subscription.cancel();
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
|||||||
|
|
||||||
controller.onListen = () async {
|
controller.onListen = () async {
|
||||||
try {
|
try {
|
||||||
await _methods.invokeMethod('reset');
|
await fido.invoke('reset');
|
||||||
await controller.sink.close();
|
await controller.sink.close();
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -102,13 +103,8 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
||||||
try {
|
try {
|
||||||
final response = jsonDecode(await _methods.invokeMethod(
|
final response = jsonDecode(
|
||||||
'setPin',
|
await fido.invoke('setPin', {'pin': oldPin, 'newPin': newPin}));
|
||||||
{
|
|
||||||
'pin': oldPin,
|
|
||||||
'newPin': newPin,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
if (response['success'] == true) {
|
if (response['success'] == true) {
|
||||||
_log.debug('FIDO PIN set/change successful');
|
_log.debug('FIDO PIN set/change successful');
|
||||||
return PinResult.success();
|
return PinResult.success();
|
||||||
@ -134,10 +130,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<PinResult> unlock(String pin) async {
|
Future<PinResult> unlock(String pin) async {
|
||||||
try {
|
try {
|
||||||
final response = jsonDecode(await _methods.invokeMethod(
|
final response = jsonDecode(await fido.invoke('unlock', {'pin': pin}));
|
||||||
'unlock',
|
|
||||||
{'pin': pin},
|
|
||||||
));
|
|
||||||
|
|
||||||
if (response['success'] == true) {
|
if (response['success'] == true) {
|
||||||
_log.debug('FIDO applet unlocked');
|
_log.debug('FIDO applet unlocked');
|
||||||
@ -165,9 +158,8 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> enableEnterpriseAttestation() async {
|
Future<void> enableEnterpriseAttestation() async {
|
||||||
try {
|
try {
|
||||||
final response = jsonDecode(await _methods.invokeMethod(
|
final response =
|
||||||
'enableEnterpriseAttestation',
|
jsonDecode(await fido.invoke('enableEnterpriseAttestation'));
|
||||||
));
|
|
||||||
|
|
||||||
if (response['success'] == true) {
|
if (response['success'] == true) {
|
||||||
_log.debug('Enterprise attestation enabled');
|
_log.debug('Enterprise attestation enabled');
|
||||||
@ -193,6 +185,8 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
|
|||||||
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||||
final _events = const EventChannel('android.fido.fingerprints');
|
final _events = const EventChannel('android.fido.fingerprints');
|
||||||
late StreamSubscription _sub;
|
late StreamSubscription _sub;
|
||||||
|
late final _FidoMethodChannelNotifier fido =
|
||||||
|
ref.read(_fidoMethodsProvider.notifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
|
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
|
||||||
@ -243,7 +237,7 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|||||||
controller.onCancel = () async {
|
controller.onCancel = () async {
|
||||||
if (!controller.isClosed) {
|
if (!controller.isClosed) {
|
||||||
_log.debug('Cancelling fingerprint registration');
|
_log.debug('Cancelling fingerprint registration');
|
||||||
await _methods.invokeMethod('cancelRegisterFingerprint');
|
await fido.invoke('cancelRegisterFingerprint');
|
||||||
await registerFpSub.cancel();
|
await registerFpSub.cancel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -251,7 +245,7 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|||||||
controller.onListen = () async {
|
controller.onListen = () async {
|
||||||
try {
|
try {
|
||||||
final registerFpResult =
|
final registerFpResult =
|
||||||
await _methods.invokeMethod('registerFingerprint', {'name': name});
|
await fido.invoke('registerFingerprint', {'name': name});
|
||||||
|
|
||||||
_log.debug('Finished registerFingerprint with: $registerFpResult');
|
_log.debug('Finished registerFingerprint with: $registerFpResult');
|
||||||
|
|
||||||
@ -286,13 +280,9 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|||||||
Future<Fingerprint> renameFingerprint(
|
Future<Fingerprint> renameFingerprint(
|
||||||
Fingerprint fingerprint, String name) async {
|
Fingerprint fingerprint, String name) async {
|
||||||
try {
|
try {
|
||||||
final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod(
|
final renameFingerprintResponse = jsonDecode(await fido.invoke(
|
||||||
'renameFingerprint',
|
'renameFingerprint',
|
||||||
{
|
{'templateId': fingerprint.templateId, 'name': name}));
|
||||||
'templateId': fingerprint.templateId,
|
|
||||||
'name': name,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
if (renameFingerprintResponse['success'] == true) {
|
if (renameFingerprintResponse['success'] == true) {
|
||||||
_log.debug('FIDO rename fingerprint succeeded');
|
_log.debug('FIDO rename fingerprint succeeded');
|
||||||
@ -316,12 +306,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
||||||
try {
|
try {
|
||||||
final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod(
|
final deleteFingerprintResponse = jsonDecode(await fido
|
||||||
'deleteFingerprint',
|
.invoke('deleteFingerprint', {'templateId': fingerprint.templateId}));
|
||||||
{
|
|
||||||
'templateId': fingerprint.templateId,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
if (deleteFingerprintResponse['success'] == true) {
|
if (deleteFingerprintResponse['success'] == true) {
|
||||||
_log.debug('FIDO delete fingerprint succeeded');
|
_log.debug('FIDO delete fingerprint succeeded');
|
||||||
@ -348,6 +334,8 @@ final androidCredentialProvider = AsyncNotifierProvider.autoDispose
|
|||||||
class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||||
final _events = const EventChannel('android.fido.credentials');
|
final _events = const EventChannel('android.fido.credentials');
|
||||||
late StreamSubscription _sub;
|
late StreamSubscription _sub;
|
||||||
|
late final _FidoMethodChannelNotifier fido =
|
||||||
|
ref.read(_fidoMethodsProvider.notifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
|
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
|
||||||
@ -371,13 +359,8 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> deleteCredential(FidoCredential credential) async {
|
Future<void> deleteCredential(FidoCredential credential) async {
|
||||||
try {
|
try {
|
||||||
await _methods.invokeMethod(
|
await fido.invoke('deleteCredential',
|
||||||
'deleteCredential',
|
{'rpId': credential.rpId, 'credentialId': credential.credentialId});
|
||||||
{
|
|
||||||
'rpId': credential.rpId,
|
|
||||||
'credentialId': credential.credentialId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} on PlatformException catch (pe) {
|
} on PlatformException catch (pe) {
|
||||||
var decodedException = pe.decode();
|
var decodedException = pe.decode();
|
||||||
if (decodedException is CancellationException) {
|
if (decodedException is CancellationException) {
|
||||||
@ -388,3 +371,11 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _fidoMethodsProvider = NotifierProvider<_FidoMethodChannelNotifier, void>(
|
||||||
|
() => _FidoMethodChannelNotifier());
|
||||||
|
|
||||||
|
class _FidoMethodChannelNotifier extends MethodChannelNotifier {
|
||||||
|
_FidoMethodChannelNotifier()
|
||||||
|
: super(const MethodChannel('android.fido.methods'));
|
||||||
|
}
|
||||||
|
@ -40,9 +40,10 @@ import 'logger.dart';
|
|||||||
import 'management/state.dart';
|
import 'management/state.dart';
|
||||||
import 'oath/otp_auth_link_handler.dart';
|
import 'oath/otp_auth_link_handler.dart';
|
||||||
import 'oath/state.dart';
|
import 'oath/state.dart';
|
||||||
|
import 'overlay/nfc/nfc_event_notifier.dart';
|
||||||
|
import 'overlay/nfc/nfc_overlay.dart';
|
||||||
import 'qr_scanner/qr_scanner_provider.dart';
|
import 'qr_scanner/qr_scanner_provider.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
import 'tap_request_dialog.dart';
|
|
||||||
import 'window_state_provider.dart';
|
import 'window_state_provider.dart';
|
||||||
|
|
||||||
Future<Widget> initialize() async {
|
Future<Widget> initialize() async {
|
||||||
@ -106,6 +107,8 @@ Future<Widget> initialize() async {
|
|||||||
child: DismissKeyboard(
|
child: DismissKeyboard(
|
||||||
child: YubicoAuthenticatorApp(page: Consumer(
|
child: YubicoAuthenticatorApp(page: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
|
ref.read(nfcEventNotifierListener).startListener(context);
|
||||||
|
|
||||||
Timer.run(() {
|
Timer.run(() {
|
||||||
ref.read(featureFlagProvider.notifier)
|
ref.read(featureFlagProvider.notifier)
|
||||||
// TODO: Load feature flags from file/config?
|
// TODO: Load feature flags from file/config?
|
||||||
@ -119,8 +122,8 @@ Future<Widget> initialize() async {
|
|||||||
// activates window state provider
|
// activates window state provider
|
||||||
ref.read(androidWindowStateProvider);
|
ref.read(androidWindowStateProvider);
|
||||||
|
|
||||||
// initializes global handler for dialogs
|
// initializes overlay for nfc events
|
||||||
ref.read(androidDialogProvider);
|
ref.read(nfcOverlay);
|
||||||
|
|
||||||
// set context which will handle otpauth links
|
// set context which will handle otpauth links
|
||||||
setupOtpAuthLinkHandler(context);
|
setupOtpAuthLinkHandler(context);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022-2023 Yubico.
|
* Copyright (C) 2022-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -36,12 +36,12 @@ import '../../exception/platform_exception_decoder.dart';
|
|||||||
import '../../oath/models.dart';
|
import '../../oath/models.dart';
|
||||||
import '../../oath/state.dart';
|
import '../../oath/state.dart';
|
||||||
import '../../widgets/toast.dart';
|
import '../../widgets/toast.dart';
|
||||||
import '../tap_request_dialog.dart';
|
import '../app_methods.dart';
|
||||||
|
import '../overlay/nfc/method_channel_notifier.dart';
|
||||||
|
import '../overlay/nfc/nfc_overlay.dart';
|
||||||
|
|
||||||
final _log = Logger('android.oath.state');
|
final _log = Logger('android.oath.state');
|
||||||
|
|
||||||
const _methods = MethodChannel('android.oath.methods');
|
|
||||||
|
|
||||||
final androidOathStateProvider = AsyncNotifierProvider.autoDispose
|
final androidOathStateProvider = AsyncNotifierProvider.autoDispose
|
||||||
.family<OathStateNotifier, OathState, DevicePath>(
|
.family<OathStateNotifier, OathState, DevicePath>(
|
||||||
_AndroidOathStateNotifier.new);
|
_AndroidOathStateNotifier.new);
|
||||||
@ -49,6 +49,8 @@ final androidOathStateProvider = AsyncNotifierProvider.autoDispose
|
|||||||
class _AndroidOathStateNotifier extends OathStateNotifier {
|
class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||||
final _events = const EventChannel('android.oath.sessionState');
|
final _events = const EventChannel('android.oath.sessionState');
|
||||||
late StreamSubscription _sub;
|
late StreamSubscription _sub;
|
||||||
|
late _OathMethodChannelNotifier oath =
|
||||||
|
ref.watch(_oathMethodsProvider.notifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<OathState> build(DevicePath arg) {
|
FutureOr<OathState> build(DevicePath arg) {
|
||||||
@ -74,10 +76,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> reset() async {
|
Future<void> reset() async {
|
||||||
try {
|
try {
|
||||||
// await ref
|
await oath.invoke('reset');
|
||||||
// .read(androidAppContextHandler)
|
|
||||||
// .switchAppContext(Application.accounts);
|
|
||||||
await _methods.invokeMethod('reset');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.debug('Calling reset failed with exception: $e');
|
_log.debug('Calling reset failed with exception: $e');
|
||||||
}
|
}
|
||||||
@ -86,8 +85,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<(bool, bool)> unlock(String password, {bool remember = false}) async {
|
Future<(bool, bool)> unlock(String password, {bool remember = false}) async {
|
||||||
try {
|
try {
|
||||||
final unlockResponse = jsonDecode(await _methods.invokeMethod(
|
final unlockResponse = jsonDecode(await oath
|
||||||
'unlock', {'password': password, 'remember': remember}));
|
.invoke('unlock', {'password': password, 'remember': remember}));
|
||||||
_log.debug('applet unlocked');
|
_log.debug('applet unlocked');
|
||||||
|
|
||||||
final unlocked = unlockResponse['unlocked'] == true;
|
final unlocked = unlockResponse['unlocked'] == true;
|
||||||
@ -108,11 +107,16 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<bool> setPassword(String? current, String password) async {
|
Future<bool> setPassword(String? current, String password) async {
|
||||||
try {
|
try {
|
||||||
await _methods.invokeMethod(
|
await oath
|
||||||
'setPassword', {'current': current, 'password': password});
|
.invoke('setPassword', {'current': current, 'password': password});
|
||||||
return true;
|
return true;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (pe) {
|
||||||
_log.debug('Calling set password failed with exception: $e');
|
final decoded = pe.decode();
|
||||||
|
if (decoded is CancellationException) {
|
||||||
|
_log.debug('Set password cancelled');
|
||||||
|
throw decoded;
|
||||||
|
}
|
||||||
|
_log.debug('Calling set password failed with exception: $pe');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,10 +124,15 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<bool> unsetPassword(String current) async {
|
Future<bool> unsetPassword(String current) async {
|
||||||
try {
|
try {
|
||||||
await _methods.invokeMethod('unsetPassword', {'current': current});
|
await oath.invoke('unsetPassword', {'current': current});
|
||||||
return true;
|
return true;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (pe) {
|
||||||
_log.debug('Calling unset password failed with exception: $e');
|
final decoded = pe.decode();
|
||||||
|
if (decoded is CancellationException) {
|
||||||
|
_log.debug('Unset password cancelled');
|
||||||
|
throw decoded;
|
||||||
|
}
|
||||||
|
_log.debug('Calling unset password failed with exception: $pe');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +140,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> forgetPassword() async {
|
Future<void> forgetPassword() async {
|
||||||
try {
|
try {
|
||||||
await _methods.invokeMethod('forgetPassword');
|
await oath.invoke('forgetPassword');
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
_log.debug('Calling forgetPassword failed with exception: $e');
|
_log.debug('Calling forgetPassword failed with exception: $e');
|
||||||
}
|
}
|
||||||
@ -146,7 +155,7 @@ Exception handlePlatformException(
|
|||||||
|
|
||||||
toast(String message, {bool popStack = false}) =>
|
toast(String message, {bool popStack = false}) =>
|
||||||
withContext((context) async {
|
withContext((context) async {
|
||||||
ref.read(androidDialogProvider).closeDialog();
|
ref.read(nfcOverlay.notifier).hide();
|
||||||
if (popStack) {
|
if (popStack) {
|
||||||
Navigator.of(context).popUntil((route) {
|
Navigator.of(context).popUntil((route) {
|
||||||
return route.isFirst;
|
return route.isFirst;
|
||||||
@ -167,7 +176,7 @@ Exception handlePlatformException(
|
|||||||
return CancellationException();
|
return CancellationException();
|
||||||
}
|
}
|
||||||
case PlatformException pe:
|
case PlatformException pe:
|
||||||
if (pe.code == 'JobCancellationException') {
|
if (pe.code == 'ContextDisposedException') {
|
||||||
// pop stack to show FIDO view
|
// pop stack to show FIDO view
|
||||||
toast(l10n.l_add_account_func_missing, popStack: true);
|
toast(l10n.l_add_account_func_missing, popStack: true);
|
||||||
return CancellationException();
|
return CancellationException();
|
||||||
@ -181,46 +190,33 @@ Exception handlePlatformException(
|
|||||||
|
|
||||||
final addCredentialToAnyProvider =
|
final addCredentialToAnyProvider =
|
||||||
Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async {
|
Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async {
|
||||||
|
final oath = ref.watch(_oathMethodsProvider.notifier);
|
||||||
try {
|
try {
|
||||||
String resultString = await _methods.invokeMethod(
|
await preserveConnectedDeviceWhenPaused();
|
||||||
'addAccountToAny', {
|
var result = jsonDecode(await oath.invoke('addAccountToAny', {
|
||||||
'uri': credentialUri.toString(),
|
'uri': credentialUri.toString(),
|
||||||
'requireTouch': requireTouch
|
'requireTouch': requireTouch
|
||||||
});
|
}));
|
||||||
|
|
||||||
var result = jsonDecode(resultString);
|
|
||||||
return OathCredential.fromJson(result['credential']);
|
return OathCredential.fromJson(result['credential']);
|
||||||
} on PlatformException catch (pe) {
|
} on PlatformException catch (pe) {
|
||||||
|
_log.error('Received exception: $pe');
|
||||||
throw handlePlatformException(ref, pe);
|
throw handlePlatformException(ref, pe);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final addCredentialsToAnyProvider = Provider(
|
final addCredentialsToAnyProvider = Provider(
|
||||||
(ref) => (List<String> credentialUris, List<bool> touchRequired) async {
|
(ref) => (List<String> credentialUris, List<bool> touchRequired) async {
|
||||||
|
final oath = ref.read(_oathMethodsProvider.notifier);
|
||||||
try {
|
try {
|
||||||
|
await preserveConnectedDeviceWhenPaused();
|
||||||
_log.debug(
|
_log.debug(
|
||||||
'Calling android with ${credentialUris.length} credentials to be added');
|
'Calling android with ${credentialUris.length} credentials to be added');
|
||||||
|
var result = jsonDecode(await oath.invoke('addAccountsToAny',
|
||||||
String resultString = await _methods.invokeMethod(
|
{'uris': credentialUris, 'requireTouch': touchRequired}));
|
||||||
'addAccountsToAny',
|
|
||||||
{
|
|
||||||
'uris': credentialUris,
|
|
||||||
'requireTouch': touchRequired,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
_log.debug('Call result: $resultString');
|
|
||||||
var result = jsonDecode(resultString);
|
|
||||||
return result['succeeded'] == credentialUris.length;
|
return result['succeeded'] == credentialUris.length;
|
||||||
} on PlatformException catch (pe) {
|
} on PlatformException catch (pe) {
|
||||||
var decodedException = pe.decode();
|
_log.error('Received exception: $pe');
|
||||||
if (decodedException is CancellationException) {
|
throw handlePlatformException(ref, pe);
|
||||||
_log.debug('User cancelled adding multiple accounts');
|
|
||||||
} else {
|
|
||||||
_log.error('Failed to add multiple accounts.', pe);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw decodedException;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,6 +234,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
final WithContext _withContext;
|
final WithContext _withContext;
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
late StreamSubscription _sub;
|
late StreamSubscription _sub;
|
||||||
|
late _OathMethodChannelNotifier oath =
|
||||||
|
_ref.read(_oathMethodsProvider.notifier);
|
||||||
|
|
||||||
_AndroidCredentialListNotifier(this._withContext, this._ref) : super() {
|
_AndroidCredentialListNotifier(this._withContext, this._ref) : super() {
|
||||||
_sub = _events.receiveBroadcastStream().listen((event) {
|
_sub = _events.receiveBroadcastStream().listen((event) {
|
||||||
@ -284,8 +282,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resultJson = await _methods
|
final resultJson =
|
||||||
.invokeMethod('calculate', {'credentialId': credential.id});
|
await oath.invoke('calculate', {'credentialId': credential.id});
|
||||||
_log.debug('Calculate', resultJson);
|
_log.debug('Calculate', resultJson);
|
||||||
return OathCode.fromJson(jsonDecode(resultJson));
|
return OathCode.fromJson(jsonDecode(resultJson));
|
||||||
} on PlatformException catch (pe) {
|
} on PlatformException catch (pe) {
|
||||||
@ -300,9 +298,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
Future<OathCredential> addAccount(Uri credentialUri,
|
Future<OathCredential> addAccount(Uri credentialUri,
|
||||||
{bool requireTouch = false}) async {
|
{bool requireTouch = false}) async {
|
||||||
try {
|
try {
|
||||||
String resultString = await _methods.invokeMethod('addAccount',
|
String resultString = await oath.invoke('addAccount',
|
||||||
{'uri': credentialUri.toString(), 'requireTouch': requireTouch});
|
{'uri': credentialUri.toString(), 'requireTouch': requireTouch});
|
||||||
|
|
||||||
var result = jsonDecode(resultString);
|
var result = jsonDecode(resultString);
|
||||||
return OathCredential.fromJson(result['credential']);
|
return OathCredential.fromJson(result['credential']);
|
||||||
} on PlatformException catch (pe) {
|
} on PlatformException catch (pe) {
|
||||||
@ -314,9 +311,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
Future<OathCredential> renameAccount(
|
Future<OathCredential> renameAccount(
|
||||||
OathCredential credential, String? issuer, String name) async {
|
OathCredential credential, String? issuer, String name) async {
|
||||||
try {
|
try {
|
||||||
final response = await _methods.invokeMethod('renameAccount',
|
final response = await oath.invoke('renameAccount',
|
||||||
{'credentialId': credential.id, 'name': name, 'issuer': issuer});
|
{'credentialId': credential.id, 'name': name, 'issuer': issuer});
|
||||||
|
|
||||||
_log.debug('Rename response: $response');
|
_log.debug('Rename response: $response');
|
||||||
|
|
||||||
var responseJson = jsonDecode(response);
|
var responseJson = jsonDecode(response);
|
||||||
@ -331,11 +327,24 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<void> deleteAccount(OathCredential credential) async {
|
Future<void> deleteAccount(OathCredential credential) async {
|
||||||
try {
|
try {
|
||||||
await _methods
|
await oath.invoke('deleteAccount', {'credentialId': credential.id});
|
||||||
.invokeMethod('deleteAccount', {'credentialId': credential.id});
|
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
_log.debug('Received exception: $e');
|
var decoded = e.decode();
|
||||||
throw e.decode();
|
if (decoded is CancellationException) {
|
||||||
|
_log.debug('Account delete was cancelled.');
|
||||||
|
} else {
|
||||||
|
_log.debug('Received exception: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _oathMethodsProvider = NotifierProvider<_OathMethodChannelNotifier, void>(
|
||||||
|
() => _OathMethodChannelNotifier());
|
||||||
|
|
||||||
|
class _OathMethodChannelNotifier extends MethodChannelNotifier {
|
||||||
|
_OathMethodChannelNotifier()
|
||||||
|
: super(const MethodChannel('android.oath.methods'));
|
||||||
|
}
|
||||||
|
36
lib/android/overlay/nfc/method_channel_notifier.dart
Normal file
36
lib/android/overlay/nfc/method_channel_notifier.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'nfc_overlay.dart';
|
||||||
|
|
||||||
|
class MethodChannelNotifier extends Notifier<void> {
|
||||||
|
final MethodChannel _channel;
|
||||||
|
|
||||||
|
MethodChannelNotifier(this._channel);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {}
|
||||||
|
|
||||||
|
Future<dynamic> invoke(String name,
|
||||||
|
[Map<String, dynamic> args = const {}]) async {
|
||||||
|
final result = await _channel.invokeMethod(name, args);
|
||||||
|
await ref.read(nfcOverlay.notifier).waitForHide();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -14,21 +14,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.yubico.authenticator.fido
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
const val dialogDescriptionFidoIndex = 200
|
part 'models.freezed.dart';
|
||||||
|
|
||||||
enum class FidoActionDescription(private val value: Int) {
|
@freezed
|
||||||
Reset(0),
|
class NfcOverlayWidgetProperties with _$NfcOverlayWidgetProperties {
|
||||||
Unlock(1),
|
factory NfcOverlayWidgetProperties({
|
||||||
SetPin(2),
|
required Widget child,
|
||||||
DeleteCredential(3),
|
@Default(false) bool visible,
|
||||||
DeleteFingerprint(4),
|
@Default(false) bool hasCloseButton,
|
||||||
RenameFingerprint(5),
|
}) = _NfcOverlayWidgetProperties;
|
||||||
RegisterFingerprint(6),
|
|
||||||
EnableEnterpriseAttestation(7),
|
|
||||||
ActionFailure(8);
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
get() = value + dialogDescriptionFidoIndex
|
|
||||||
}
|
}
|
189
lib/android/overlay/nfc/models.freezed.dart
Normal file
189
lib/android/overlay/nfc/models.freezed.dart
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'models.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$NfcOverlayWidgetProperties {
|
||||||
|
Widget get child => throw _privateConstructorUsedError;
|
||||||
|
bool get visible => throw _privateConstructorUsedError;
|
||||||
|
bool get hasCloseButton => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of NfcOverlayWidgetProperties
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$NfcOverlayWidgetPropertiesCopyWith<NfcOverlayWidgetProperties>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $NfcOverlayWidgetPropertiesCopyWith<$Res> {
|
||||||
|
factory $NfcOverlayWidgetPropertiesCopyWith(NfcOverlayWidgetProperties value,
|
||||||
|
$Res Function(NfcOverlayWidgetProperties) then) =
|
||||||
|
_$NfcOverlayWidgetPropertiesCopyWithImpl<$Res,
|
||||||
|
NfcOverlayWidgetProperties>;
|
||||||
|
@useResult
|
||||||
|
$Res call({Widget child, bool visible, bool hasCloseButton});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$NfcOverlayWidgetPropertiesCopyWithImpl<$Res,
|
||||||
|
$Val extends NfcOverlayWidgetProperties>
|
||||||
|
implements $NfcOverlayWidgetPropertiesCopyWith<$Res> {
|
||||||
|
_$NfcOverlayWidgetPropertiesCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of NfcOverlayWidgetProperties
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? child = null,
|
||||||
|
Object? visible = null,
|
||||||
|
Object? hasCloseButton = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
child: null == child
|
||||||
|
? _value.child
|
||||||
|
: child // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Widget,
|
||||||
|
visible: null == visible
|
||||||
|
? _value.visible
|
||||||
|
: visible // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hasCloseButton: null == hasCloseButton
|
||||||
|
? _value.hasCloseButton
|
||||||
|
: hasCloseButton // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$NfcOverlayWidgetPropertiesImplCopyWith<$Res>
|
||||||
|
implements $NfcOverlayWidgetPropertiesCopyWith<$Res> {
|
||||||
|
factory _$$NfcOverlayWidgetPropertiesImplCopyWith(
|
||||||
|
_$NfcOverlayWidgetPropertiesImpl value,
|
||||||
|
$Res Function(_$NfcOverlayWidgetPropertiesImpl) then) =
|
||||||
|
__$$NfcOverlayWidgetPropertiesImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({Widget child, bool visible, bool hasCloseButton});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$NfcOverlayWidgetPropertiesImplCopyWithImpl<$Res>
|
||||||
|
extends _$NfcOverlayWidgetPropertiesCopyWithImpl<$Res,
|
||||||
|
_$NfcOverlayWidgetPropertiesImpl>
|
||||||
|
implements _$$NfcOverlayWidgetPropertiesImplCopyWith<$Res> {
|
||||||
|
__$$NfcOverlayWidgetPropertiesImplCopyWithImpl(
|
||||||
|
_$NfcOverlayWidgetPropertiesImpl _value,
|
||||||
|
$Res Function(_$NfcOverlayWidgetPropertiesImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of NfcOverlayWidgetProperties
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? child = null,
|
||||||
|
Object? visible = null,
|
||||||
|
Object? hasCloseButton = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$NfcOverlayWidgetPropertiesImpl(
|
||||||
|
child: null == child
|
||||||
|
? _value.child
|
||||||
|
: child // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Widget,
|
||||||
|
visible: null == visible
|
||||||
|
? _value.visible
|
||||||
|
: visible // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hasCloseButton: null == hasCloseButton
|
||||||
|
? _value.hasCloseButton
|
||||||
|
: hasCloseButton // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$NfcOverlayWidgetPropertiesImpl implements _NfcOverlayWidgetProperties {
|
||||||
|
_$NfcOverlayWidgetPropertiesImpl(
|
||||||
|
{required this.child, this.visible = false, this.hasCloseButton = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Widget child;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool visible;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool hasCloseButton;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NfcOverlayWidgetProperties(child: $child, visible: $visible, hasCloseButton: $hasCloseButton)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$NfcOverlayWidgetPropertiesImpl &&
|
||||||
|
(identical(other.child, child) || other.child == child) &&
|
||||||
|
(identical(other.visible, visible) || other.visible == visible) &&
|
||||||
|
(identical(other.hasCloseButton, hasCloseButton) ||
|
||||||
|
other.hasCloseButton == hasCloseButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, child, visible, hasCloseButton);
|
||||||
|
|
||||||
|
/// Create a copy of NfcOverlayWidgetProperties
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$NfcOverlayWidgetPropertiesImplCopyWith<_$NfcOverlayWidgetPropertiesImpl>
|
||||||
|
get copyWith => __$$NfcOverlayWidgetPropertiesImplCopyWithImpl<
|
||||||
|
_$NfcOverlayWidgetPropertiesImpl>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _NfcOverlayWidgetProperties
|
||||||
|
implements NfcOverlayWidgetProperties {
|
||||||
|
factory _NfcOverlayWidgetProperties(
|
||||||
|
{required final Widget child,
|
||||||
|
final bool visible,
|
||||||
|
final bool hasCloseButton}) = _$NfcOverlayWidgetPropertiesImpl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget get child;
|
||||||
|
@override
|
||||||
|
bool get visible;
|
||||||
|
@override
|
||||||
|
bool get hasCloseButton;
|
||||||
|
|
||||||
|
/// Create a copy of NfcOverlayWidgetProperties
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$NfcOverlayWidgetPropertiesImplCopyWith<_$NfcOverlayWidgetPropertiesImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
123
lib/android/overlay/nfc/nfc_event_notifier.dart
Normal file
123
lib/android/overlay/nfc/nfc_event_notifier.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../../app/logging.dart';
|
||||||
|
import '../../../app/state.dart';
|
||||||
|
import 'nfc_overlay.dart';
|
||||||
|
import 'views/nfc_overlay_widget.dart';
|
||||||
|
|
||||||
|
final _log = Logger('android.nfc_event_notifier');
|
||||||
|
|
||||||
|
class NfcEvent {
|
||||||
|
const NfcEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcHideViewEvent extends NfcEvent {
|
||||||
|
final Duration delay;
|
||||||
|
|
||||||
|
const NfcHideViewEvent({this.delay = Duration.zero});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcSetViewEvent extends NfcEvent {
|
||||||
|
final Widget child;
|
||||||
|
final bool showIfHidden;
|
||||||
|
|
||||||
|
const NfcSetViewEvent({required this.child, this.showIfHidden = true});
|
||||||
|
}
|
||||||
|
|
||||||
|
final nfcEventNotifier =
|
||||||
|
NotifierProvider<_NfcEventNotifier, NfcEvent>(_NfcEventNotifier.new);
|
||||||
|
|
||||||
|
class _NfcEventNotifier extends Notifier<NfcEvent> {
|
||||||
|
@override
|
||||||
|
NfcEvent build() {
|
||||||
|
return const NfcEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void send(NfcEvent event) {
|
||||||
|
state = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final nfcEventNotifierListener = Provider<_NfcEventNotifierListener>(
|
||||||
|
(ref) => _NfcEventNotifierListener(ref));
|
||||||
|
|
||||||
|
class _NfcEventNotifierListener {
|
||||||
|
final ProviderRef _ref;
|
||||||
|
ProviderSubscription<NfcEvent>? listener;
|
||||||
|
|
||||||
|
_NfcEventNotifierListener(this._ref);
|
||||||
|
|
||||||
|
void startListener(BuildContext context) {
|
||||||
|
listener?.close();
|
||||||
|
listener = _ref.listen(nfcEventNotifier, (previous, action) {
|
||||||
|
_log.debug('Event change: $previous -> $action');
|
||||||
|
switch (action) {
|
||||||
|
case (NfcSetViewEvent a):
|
||||||
|
if (!visible && a.showIfHidden) {
|
||||||
|
_show(context, a.child);
|
||||||
|
} else {
|
||||||
|
_ref
|
||||||
|
.read(nfcOverlayWidgetProperties.notifier)
|
||||||
|
.update(child: a.child);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (NfcHideViewEvent e):
|
||||||
|
_hide(context, e.delay);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show(BuildContext context, Widget child) async {
|
||||||
|
final notifier = _ref.read(nfcOverlayWidgetProperties.notifier);
|
||||||
|
notifier.update(child: child);
|
||||||
|
if (!visible) {
|
||||||
|
visible = true;
|
||||||
|
final result = await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const NfcOverlayWidget();
|
||||||
|
});
|
||||||
|
if (result == null) {
|
||||||
|
// the modal sheet was cancelled by Back button, close button or dismiss
|
||||||
|
_ref.read(nfcOverlay.notifier).onCancel();
|
||||||
|
}
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hide(BuildContext context, Duration timeout) {
|
||||||
|
Future.delayed(timeout, () {
|
||||||
|
_ref.read(withContextProvider)((context) async {
|
||||||
|
if (visible) {
|
||||||
|
Navigator.of(context).pop('HIDDEN');
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get visible =>
|
||||||
|
_ref.read(nfcOverlayWidgetProperties.select((s) => s.visible));
|
||||||
|
|
||||||
|
set visible(bool visible) =>
|
||||||
|
_ref.read(nfcOverlayWidgetProperties.notifier).update(visible: visible);
|
||||||
|
}
|
164
lib/android/overlay/nfc/nfc_overlay.dart
Executable file
164
lib/android/overlay/nfc/nfc_overlay.dart
Executable file
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022-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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../../app/logging.dart';
|
||||||
|
import '../../../app/state.dart';
|
||||||
|
import '../../state.dart';
|
||||||
|
import 'nfc_event_notifier.dart';
|
||||||
|
import 'views/nfc_content_widget.dart';
|
||||||
|
import 'views/nfc_overlay_icons.dart';
|
||||||
|
import 'views/nfc_overlay_widget.dart';
|
||||||
|
|
||||||
|
final _log = Logger('android.nfc_overlay');
|
||||||
|
const _channel = MethodChannel('com.yubico.authenticator.channel.nfc_overlay');
|
||||||
|
|
||||||
|
final nfcOverlay =
|
||||||
|
NotifierProvider<_NfcOverlayNotifier, int>(_NfcOverlayNotifier.new);
|
||||||
|
|
||||||
|
class _NfcOverlayNotifier extends Notifier<int> {
|
||||||
|
Timer? processingViewTimeout;
|
||||||
|
late final l10n = ref.read(l10nProvider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int build() {
|
||||||
|
ref.listen(androidNfcState, (previous, current) {
|
||||||
|
_log.debug('Received nfc state: $current');
|
||||||
|
processingViewTimeout?.cancel();
|
||||||
|
final notifier = ref.read(nfcEventNotifier.notifier);
|
||||||
|
|
||||||
|
switch (current) {
|
||||||
|
case NfcState.ongoing:
|
||||||
|
// the "Hold still..." view will be shown after this timeout
|
||||||
|
// if the action is finished before, the timer might be cancelled
|
||||||
|
// causing the view not to be visible at all
|
||||||
|
const timeout = 300;
|
||||||
|
processingViewTimeout =
|
||||||
|
Timer(const Duration(milliseconds: timeout), () {
|
||||||
|
notifier.send(showHoldStill());
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case NfcState.success:
|
||||||
|
notifier.send(showDone());
|
||||||
|
notifier
|
||||||
|
.send(const NfcHideViewEvent(delay: Duration(milliseconds: 400)));
|
||||||
|
break;
|
||||||
|
case NfcState.failure:
|
||||||
|
notifier.send(showFailed());
|
||||||
|
notifier
|
||||||
|
.send(const NfcHideViewEvent(delay: Duration(milliseconds: 800)));
|
||||||
|
break;
|
||||||
|
case NfcState.disabled:
|
||||||
|
_log.debug('Received state: disabled');
|
||||||
|
break;
|
||||||
|
case NfcState.idle:
|
||||||
|
_log.debug('Received state: idle');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_channel.setMethodCallHandler((call) async {
|
||||||
|
final notifier = ref.read(nfcEventNotifier.notifier);
|
||||||
|
switch (call.method) {
|
||||||
|
case 'show':
|
||||||
|
notifier.send(showTapYourYubiKey());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'close':
|
||||||
|
hide();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'NotImplemented',
|
||||||
|
message: 'Method ${call.method} is not implemented',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NfcEvent showTapYourYubiKey() {
|
||||||
|
ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: true);
|
||||||
|
return NfcSetViewEvent(
|
||||||
|
child: NfcContentWidget(
|
||||||
|
title: l10n.s_nfc_ready_to_scan,
|
||||||
|
subtitle: l10n.s_nfc_tap_your_yubikey,
|
||||||
|
icon: const NfcIconProgressBar(false),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
NfcEvent showHoldStill() {
|
||||||
|
ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false);
|
||||||
|
return NfcSetViewEvent(
|
||||||
|
child: NfcContentWidget(
|
||||||
|
title: l10n.s_nfc_ready_to_scan,
|
||||||
|
subtitle: l10n.s_nfc_hold_still,
|
||||||
|
icon: const NfcIconProgressBar(true),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
NfcEvent showDone() {
|
||||||
|
ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false);
|
||||||
|
return NfcSetViewEvent(
|
||||||
|
child: NfcContentWidget(
|
||||||
|
title: l10n.s_nfc_ready_to_scan,
|
||||||
|
subtitle: l10n.s_done,
|
||||||
|
icon: const NfcIconSuccess(),
|
||||||
|
),
|
||||||
|
showIfHidden: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
NfcEvent showFailed() {
|
||||||
|
ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false);
|
||||||
|
return NfcSetViewEvent(
|
||||||
|
child: NfcContentWidget(
|
||||||
|
title: l10n.s_nfc_ready_to_scan,
|
||||||
|
subtitle: l10n.l_nfc_failed_to_scan,
|
||||||
|
icon: const NfcIconFailure(),
|
||||||
|
),
|
||||||
|
showIfHidden: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hide() {
|
||||||
|
ref.read(nfcEventNotifier.notifier).send(const NfcHideViewEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCancel() async {
|
||||||
|
await _channel.invokeMethod('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> waitForHide() async {
|
||||||
|
final completer = Completer();
|
||||||
|
|
||||||
|
Timer.periodic(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
(timer) {
|
||||||
|
if (ref.read(nfcOverlayWidgetProperties.select((s) => !s.visible))) {
|
||||||
|
timer.cancel();
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await completer.future;
|
||||||
|
}
|
||||||
|
}
|
55
lib/android/overlay/nfc/views/nfc_content_widget.dart
Normal file
55
lib/android/overlay/nfc/views/nfc_content_widget.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class NfcContentWidget extends ConsumerWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
const NfcContentWidget({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final textTheme = theme.textTheme;
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(title, textAlign: TextAlign.center, style: textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(subtitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: textTheme.titleMedium!.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
icon,
|
||||||
|
const SizedBox(height: 24)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
79
lib/android/overlay/nfc/views/nfc_overlay_icons.dart
Normal file
79
lib/android/overlay/nfc/views/nfc_overlay_icons.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
class NfcIconProgressBar extends StatelessWidget {
|
||||||
|
final bool inProgress;
|
||||||
|
|
||||||
|
const NfcIconProgressBar(this.inProgress, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
children: [
|
||||||
|
const Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Icon(Symbols.contactless),
|
||||||
|
),
|
||||||
|
const ClipOval(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
child: OverflowBox(
|
||||||
|
maxWidth: double.infinity,
|
||||||
|
maxHeight: double.infinity,
|
||||||
|
child: Icon(Symbols.contactless),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
child: CircularProgressIndicator(value: inProgress ? null : 1.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcIconSuccess extends StatelessWidget {
|
||||||
|
const NfcIconSuccess({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Icon(
|
||||||
|
Symbols.check,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcIconFailure extends StatelessWidget {
|
||||||
|
const NfcIconFailure({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Icon(
|
||||||
|
Symbols.close,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
);
|
||||||
|
}
|
76
lib/android/overlay/nfc/views/nfc_overlay_widget.dart
Normal file
76
lib/android/overlay/nfc/views/nfc_overlay_widget.dart
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import '../models.dart';
|
||||||
|
|
||||||
|
final nfcOverlayWidgetProperties =
|
||||||
|
NotifierProvider<_NfcOverlayWidgetProperties, NfcOverlayWidgetProperties>(
|
||||||
|
_NfcOverlayWidgetProperties.new);
|
||||||
|
|
||||||
|
class _NfcOverlayWidgetProperties extends Notifier<NfcOverlayWidgetProperties> {
|
||||||
|
@override
|
||||||
|
NfcOverlayWidgetProperties build() {
|
||||||
|
return NfcOverlayWidgetProperties(child: const SizedBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
void update({
|
||||||
|
Widget? child,
|
||||||
|
bool? visible,
|
||||||
|
bool? hasCloseButton,
|
||||||
|
}) {
|
||||||
|
state = state.copyWith(
|
||||||
|
child: child ?? state.child,
|
||||||
|
visible: visible ?? state.visible,
|
||||||
|
hasCloseButton: hasCloseButton ?? state.hasCloseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcOverlayWidget extends ConsumerWidget {
|
||||||
|
const NfcOverlayWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final widget = ref.watch(nfcOverlayWidgetProperties.select((s) => s.child));
|
||||||
|
final showCloseButton =
|
||||||
|
ref.watch(nfcOverlayWidgetProperties.select((s) => s.hasCloseButton));
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Stack(fit: StackFit.passthrough, children: [
|
||||||
|
if (showCloseButton)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Symbols.close, fill: 1, size: 24)),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
|
||||||
|
child: widget,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -69,22 +69,50 @@ class _AndroidClipboard extends AppClipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NfcStateNotifier extends StateNotifier<bool> {
|
class NfcAdapterState extends StateNotifier<bool> {
|
||||||
NfcStateNotifier() : super(false);
|
NfcAdapterState() : super(false);
|
||||||
|
|
||||||
void setNfcEnabled(bool value) {
|
void enable(bool value) {
|
||||||
state = value;
|
state = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum NfcState {
|
||||||
|
disabled,
|
||||||
|
idle,
|
||||||
|
ongoing,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
class NfcStateNotifier extends StateNotifier<NfcState> {
|
||||||
|
NfcStateNotifier() : super(NfcState.disabled);
|
||||||
|
|
||||||
|
void set(int stateValue) {
|
||||||
|
var newState = switch (stateValue) {
|
||||||
|
0 => NfcState.disabled,
|
||||||
|
1 => NfcState.idle,
|
||||||
|
2 => NfcState.ongoing,
|
||||||
|
3 => NfcState.success,
|
||||||
|
4 => NfcState.failure,
|
||||||
|
_ => NfcState.disabled
|
||||||
|
};
|
||||||
|
|
||||||
|
state = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final androidSectionPriority = Provider<List<Section>>((ref) => []);
|
final androidSectionPriority = Provider<List<Section>>((ref) => []);
|
||||||
|
|
||||||
final androidSdkVersionProvider = Provider<int>((ref) => -1);
|
final androidSdkVersionProvider = Provider<int>((ref) => -1);
|
||||||
|
|
||||||
final androidNfcSupportProvider = Provider<bool>((ref) => false);
|
final androidNfcSupportProvider = Provider<bool>((ref) => false);
|
||||||
|
|
||||||
final androidNfcStateProvider =
|
final androidNfcAdapterState =
|
||||||
StateNotifierProvider<NfcStateNotifier, bool>((ref) => NfcStateNotifier());
|
StateNotifierProvider<NfcAdapterState, bool>((ref) => NfcAdapterState());
|
||||||
|
|
||||||
|
final androidNfcState = StateNotifierProvider<NfcStateNotifier, NfcState>(
|
||||||
|
(ref) => NfcStateNotifier());
|
||||||
|
|
||||||
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||||
if (ref.read(androidSdkVersionProvider) < 29) {
|
if (ref.read(androidSdkVersionProvider) < 29) {
|
||||||
@ -191,6 +219,7 @@ class NfcTapActionNotifier extends StateNotifier<NfcTapAction> {
|
|||||||
static const _prefNfcOpenApp = 'prefNfcOpenApp';
|
static const _prefNfcOpenApp = 'prefNfcOpenApp';
|
||||||
static const _prefNfcCopyOtp = 'prefNfcCopyOtp';
|
static const _prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
NfcTapActionNotifier._(this._prefs, super._state);
|
NfcTapActionNotifier._(this._prefs, super._state);
|
||||||
|
|
||||||
factory NfcTapActionNotifier(SharedPreferences prefs) {
|
factory NfcTapActionNotifier(SharedPreferences prefs) {
|
||||||
@ -232,6 +261,7 @@ class NfcKbdLayoutNotifier extends StateNotifier<String> {
|
|||||||
static const String _defaultClipKbdLayout = 'US';
|
static const String _defaultClipKbdLayout = 'US';
|
||||||
static const _prefClipKbdLayout = 'prefClipKbdLayout';
|
static const _prefClipKbdLayout = 'prefClipKbdLayout';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
NfcKbdLayoutNotifier(this._prefs)
|
NfcKbdLayoutNotifier(this._prefs)
|
||||||
: super(_prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout);
|
: super(_prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout);
|
||||||
|
|
||||||
@ -250,6 +280,7 @@ final androidNfcBypassTouchProvider =
|
|||||||
class NfcBypassTouchNotifier extends StateNotifier<bool> {
|
class NfcBypassTouchNotifier extends StateNotifier<bool> {
|
||||||
static const _prefNfcBypassTouch = 'prefNfcBypassTouch';
|
static const _prefNfcBypassTouch = 'prefNfcBypassTouch';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
NfcBypassTouchNotifier(this._prefs)
|
NfcBypassTouchNotifier(this._prefs)
|
||||||
: super(_prefs.getBool(_prefNfcBypassTouch) ?? false);
|
: super(_prefs.getBool(_prefNfcBypassTouch) ?? false);
|
||||||
|
|
||||||
@ -268,6 +299,7 @@ final androidNfcSilenceSoundsProvider =
|
|||||||
class NfcSilenceSoundsNotifier extends StateNotifier<bool> {
|
class NfcSilenceSoundsNotifier extends StateNotifier<bool> {
|
||||||
static const _prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
static const _prefNfcSilenceSounds = 'prefNfcSilenceSounds';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
NfcSilenceSoundsNotifier(this._prefs)
|
NfcSilenceSoundsNotifier(this._prefs)
|
||||||
: super(_prefs.getBool(_prefNfcSilenceSounds) ?? false);
|
: super(_prefs.getBool(_prefNfcSilenceSounds) ?? false);
|
||||||
|
|
||||||
@ -286,6 +318,7 @@ final androidUsbLaunchAppProvider =
|
|||||||
class UsbLaunchAppNotifier extends StateNotifier<bool> {
|
class UsbLaunchAppNotifier extends StateNotifier<bool> {
|
||||||
static const _prefUsbOpenApp = 'prefUsbOpenApp';
|
static const _prefUsbOpenApp = 'prefUsbOpenApp';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
UsbLaunchAppNotifier(this._prefs)
|
UsbLaunchAppNotifier(this._prefs)
|
||||||
: super(_prefs.getBool(_prefUsbOpenApp) ?? false);
|
: super(_prefs.getBool(_prefUsbOpenApp) ?? false);
|
||||||
|
|
||||||
|
@ -1,231 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022-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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
import '../app/state.dart';
|
|
||||||
import '../app/views/user_interaction.dart';
|
|
||||||
|
|
||||||
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
|
|
||||||
|
|
||||||
// _DIcon identifies the icon which should be displayed on the dialog
|
|
||||||
enum _DIcon {
|
|
||||||
nfcIcon,
|
|
||||||
successIcon,
|
|
||||||
failureIcon,
|
|
||||||
invalid;
|
|
||||||
|
|
||||||
static _DIcon fromId(int? id) =>
|
|
||||||
const {
|
|
||||||
0: _DIcon.nfcIcon,
|
|
||||||
1: _DIcon.successIcon,
|
|
||||||
2: _DIcon.failureIcon
|
|
||||||
}[id] ??
|
|
||||||
_DIcon.invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _DDesc contains id of title resource for the dialog
|
|
||||||
enum _DTitle {
|
|
||||||
tapKey,
|
|
||||||
operationSuccessful,
|
|
||||||
operationFailed,
|
|
||||||
invalid;
|
|
||||||
|
|
||||||
static _DTitle fromId(int? id) =>
|
|
||||||
const {
|
|
||||||
0: _DTitle.tapKey,
|
|
||||||
1: _DTitle.operationSuccessful,
|
|
||||||
2: _DTitle.operationFailed
|
|
||||||
}[id] ??
|
|
||||||
_DTitle.invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _DDesc contains action description in the dialog
|
|
||||||
enum _DDesc {
|
|
||||||
// oath descriptions
|
|
||||||
oathResetApplet,
|
|
||||||
oathUnlockSession,
|
|
||||||
oathSetPassword,
|
|
||||||
oathUnsetPassword,
|
|
||||||
oathAddAccount,
|
|
||||||
oathRenameAccount,
|
|
||||||
oathDeleteAccount,
|
|
||||||
oathCalculateCode,
|
|
||||||
oathActionFailure,
|
|
||||||
oathAddMultipleAccounts,
|
|
||||||
// FIDO descriptions
|
|
||||||
fidoResetApplet,
|
|
||||||
fidoUnlockSession,
|
|
||||||
fidoSetPin,
|
|
||||||
fidoDeleteCredential,
|
|
||||||
fidoDeleteFingerprint,
|
|
||||||
fidoRenameFingerprint,
|
|
||||||
fidoRegisterFingerprint,
|
|
||||||
fidoEnableEnterpriseAttestation,
|
|
||||||
fidoActionFailure,
|
|
||||||
// Others
|
|
||||||
invalid;
|
|
||||||
|
|
||||||
static const int dialogDescriptionOathIndex = 100;
|
|
||||||
static const int dialogDescriptionFidoIndex = 200;
|
|
||||||
|
|
||||||
static _DDesc fromId(int? id) =>
|
|
||||||
const {
|
|
||||||
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: fidoDeleteCredential,
|
|
||||||
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
|
|
||||||
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
|
|
||||||
dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint,
|
|
||||||
dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation,
|
|
||||||
dialogDescriptionFidoIndex + 8: fidoActionFailure,
|
|
||||||
}[id] ??
|
|
||||||
_DDesc.invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
final androidDialogProvider = Provider<_DialogProvider>(
|
|
||||||
(ref) {
|
|
||||||
return _DialogProvider(ref.watch(withContextProvider));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
class _DialogProvider {
|
|
||||||
final WithContext _withContext;
|
|
||||||
UserInteractionController? _controller;
|
|
||||||
|
|
||||||
_DialogProvider(this._withContext) {
|
|
||||||
_channel.setMethodCallHandler((call) async {
|
|
||||||
final args = jsonDecode(call.arguments);
|
|
||||||
switch (call.method) {
|
|
||||||
case 'close':
|
|
||||||
closeDialog();
|
|
||||||
break;
|
|
||||||
case 'show':
|
|
||||||
await _showDialog(args['title'], args['description'], args['icon']);
|
|
||||||
break;
|
|
||||||
case 'state':
|
|
||||||
await _updateDialogState(
|
|
||||||
args['title'], args['description'], args['icon']);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw PlatformException(
|
|
||||||
code: 'NotImplemented',
|
|
||||||
message: 'Method ${call.method} is not implemented',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeDialog() {
|
|
||||||
_controller?.close();
|
|
||||||
_controller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? _getIcon(int? icon) => switch (_DIcon.fromId(icon)) {
|
|
||||||
_DIcon.nfcIcon => const Icon(Symbols.contactless),
|
|
||||||
_DIcon.successIcon => const Icon(Symbols.check_circle),
|
|
||||||
_DIcon.failureIcon => const Icon(Symbols.error),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
String _getTitle(BuildContext context, int? titleId) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return switch (_DTitle.fromId(titleId)) {
|
|
||||||
_DTitle.tapKey => l10n.l_nfc_dialog_tap_key,
|
|
||||||
_DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success,
|
|
||||||
_DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed,
|
|
||||||
_ => ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getDialogDescription(BuildContext context, int? descriptionId) {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return switch (_DDesc.fromId(descriptionId)) {
|
|
||||||
_DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset,
|
|
||||||
_DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock,
|
|
||||||
_DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password,
|
|
||||||
_DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password,
|
|
||||||
_DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account,
|
|
||||||
_DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account,
|
|
||||||
_DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account,
|
|
||||||
_DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code,
|
|
||||||
_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.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,
|
|
||||||
_ => ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateDialogState(
|
|
||||||
int? title, int? description, int? dialogIcon) async {
|
|
||||||
final icon = _getIcon(dialogIcon);
|
|
||||||
await _withContext((context) async {
|
|
||||||
_controller?.updateContent(
|
|
||||||
title: _getTitle(context, title),
|
|
||||||
description: _getDialogDescription(context, description),
|
|
||||||
icon: icon != null
|
|
||||||
? IconTheme(
|
|
||||||
data: IconTheme.of(context).copyWith(size: 64),
|
|
||||||
child: icon,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showDialog(int title, int description, int? dialogIcon) async {
|
|
||||||
final icon = _getIcon(dialogIcon);
|
|
||||||
_controller = await _withContext((context) async => promptUserInteraction(
|
|
||||||
context,
|
|
||||||
title: _getTitle(context, title),
|
|
||||||
description: _getDialogDescription(context, description),
|
|
||||||
icon: icon != null
|
|
||||||
? IconTheme(
|
|
||||||
data: IconTheme.of(context).copyWith(size: 64),
|
|
||||||
child: icon,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onCancel: () {
|
|
||||||
_channel.invokeMethod('cancel');
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022-2023 Yubico.
|
* Copyright (C) 2022-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -58,7 +58,7 @@ class _WindowStateNotifier extends StateNotifier<WindowState>
|
|||||||
if (lifeCycleState == AppLifecycleState.resumed) {
|
if (lifeCycleState == AppLifecycleState.resumed) {
|
||||||
_log.debug('Reading nfc enabled value');
|
_log.debug('Reading nfc enabled value');
|
||||||
isNfcEnabled().then((value) =>
|
isNfcEnabled().then((value) =>
|
||||||
_ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
|
_ref.read(androidNfcAdapterState.notifier).enable(value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_log.debug('Ignoring appLifecycleStateChange');
|
_log.debug('Ignoring appLifecycleStateChange');
|
||||||
|
@ -71,7 +71,7 @@ class DevicePickerContent extends ConsumerWidget {
|
|||||||
Widget? androidNoKeyWidget;
|
Widget? androidNoKeyWidget;
|
||||||
if (isAndroid && devices.isEmpty) {
|
if (isAndroid && devices.isEmpty) {
|
||||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
var isNfcEnabled = ref.watch(androidNfcAdapterState);
|
||||||
final subtitle = hasNfcSupport && isNfcEnabled
|
final subtitle = hasNfcSupport && isNfcEnabled
|
||||||
? l10n.l_insert_or_tap_yk
|
? l10n.l_insert_or_tap_yk
|
||||||
: l10n.l_insert_yk;
|
: l10n.l_insert_yk;
|
||||||
|
@ -52,12 +52,21 @@ class MainPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
isNfcEnabled().then((value) =>
|
isNfcEnabled().then(
|
||||||
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
|
(value) => ref.read(androidNfcAdapterState.notifier).enable(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current device changes, we need to pop any open dialogs.
|
// If the current device changes, we need to pop any open dialogs.
|
||||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, __) {
|
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider,
|
||||||
|
(prev, next) {
|
||||||
|
final serial = next.hasValue == true ? next.value?.info.serial : null;
|
||||||
|
final prevSerial =
|
||||||
|
prev?.hasValue == true ? prev?.value?.info.serial : null;
|
||||||
|
if ((serial != null && serial == prevSerial) ||
|
||||||
|
(next.hasValue && (prev != null && prev.isLoading))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.of(context).popUntil((route) {
|
Navigator.of(context).popUntil((route) {
|
||||||
return route.isFirst ||
|
return route.isFirst ||
|
||||||
[
|
[
|
||||||
@ -69,7 +78,6 @@ class MainPage extends ConsumerWidget {
|
|||||||
'oath_add_account',
|
'oath_add_account',
|
||||||
'oath_icon_pack_dialog',
|
'oath_icon_pack_dialog',
|
||||||
'android_qr_scanner_view',
|
'android_qr_scanner_view',
|
||||||
'android_alert_dialog'
|
|
||||||
].contains(route.settings.name);
|
].contains(route.settings.name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -84,7 +92,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
if (deviceNode == null) {
|
if (deviceNode == null) {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
var isNfcEnabled = ref.watch(androidNfcAdapterState);
|
||||||
return HomeMessagePage(
|
return HomeMessagePage(
|
||||||
centered: true,
|
centered: true,
|
||||||
graphic: noKeyImage,
|
graphic: noKeyImage,
|
||||||
@ -103,6 +111,10 @@ class MainPage extends ConsumerWidget {
|
|||||||
label: Text(l10n.s_add_account),
|
label: Text(l10n.s_add_account),
|
||||||
icon: const Icon(Symbols.person_add_alt),
|
icon: const Icon(Symbols.person_add_alt),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// make sure we execute the "Add account" in OATH section
|
||||||
|
ref
|
||||||
|
.read(currentSectionProvider.notifier)
|
||||||
|
.setCurrentSection(Section.accounts);
|
||||||
await addOathAccount(context, ref);
|
await addOathAccount(context, ref);
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
@ -46,7 +46,7 @@ class MessagePageNotInitialized extends ConsumerWidget {
|
|||||||
|
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
|
||||||
var isNfcEnabled = ref.watch(androidNfcStateProvider);
|
var isNfcEnabled = ref.watch(androidNfcAdapterState);
|
||||||
var isUsbYubiKey =
|
var isUsbYubiKey =
|
||||||
ref.watch(attachedDevicesProvider).firstOrNull?.transport ==
|
ref.watch(attachedDevicesProvider).firstOrNull?.transport ==
|
||||||
Transport.usb;
|
Transport.usb;
|
||||||
|
@ -280,6 +280,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _submit() async {
|
void _submit() async {
|
||||||
|
_currentPinFocus.unfocus();
|
||||||
|
_newPinFocus.unfocus();
|
||||||
|
_confirmPinFocus.unfocus();
|
||||||
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final oldPin = _currentPinController.text.isNotEmpty
|
final oldPin = _currentPinController.text.isNotEmpty
|
||||||
? _currentPinController.text
|
? _currentPinController.text
|
||||||
|
@ -30,6 +30,7 @@ import '../state.dart';
|
|||||||
class PinEntryForm extends ConsumerStatefulWidget {
|
class PinEntryForm extends ConsumerStatefulWidget {
|
||||||
final FidoState _state;
|
final FidoState _state;
|
||||||
final DeviceNode _deviceNode;
|
final DeviceNode _deviceNode;
|
||||||
|
|
||||||
const PinEntryForm(this._state, this._deviceNode, {super.key});
|
const PinEntryForm(this._state, this._deviceNode, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,6 +59,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _submit() async {
|
void _submit() async {
|
||||||
|
_pinFocus.unfocus();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_pinIsWrong = false;
|
_pinIsWrong = false;
|
||||||
_isObscure = true;
|
_isObscure = true;
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen",
|
"l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen",
|
||||||
"s_allow_screenshots": "Bildschirmfotos erlauben",
|
"s_allow_screenshots": "Bildschirmfotos erlauben",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "Halten Sie Ihren Schlüssel dagegen",
|
|
||||||
"s_nfc_dialog_operation_success": "Erfolgreich",
|
|
||||||
"s_nfc_dialog_operation_failed": "Fehlgeschlagen",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "Aktion: OATH-Anwendung zurücksetzen",
|
|
||||||
"s_nfc_dialog_oath_unlock": "Aktion: OATH-Anwendung entsperren",
|
|
||||||
"s_nfc_dialog_oath_set_password": "Aktion: OATH-Passwort setzen",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "Aktion: OATH-Passwort entfernen",
|
|
||||||
"s_nfc_dialog_oath_add_account": "Aktion: neues Konto hinzufügen",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "Aktion: Konto umbenennen",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "Aktion: Konto löschen",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "Aktion: OATH-Code berechnen",
|
|
||||||
"s_nfc_dialog_oath_failure": "OATH-Operation fehlgeschlagen",
|
|
||||||
"s_nfc_dialog_oath_add_multiple_accounts": "Aktion: mehrere Konten hinzufügen",
|
|
||||||
|
|
||||||
"s_nfc_dialog_fido_reset": "Aktion: FIDO-Anwendung zurücksetzen",
|
|
||||||
"s_nfc_dialog_fido_unlock": "Aktion: FIDO-Anwendung entsperren",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "Aktion: FIDO-PIN setzen oder ändern",
|
|
||||||
"s_nfc_dialog_fido_delete_credential": "Aktion: Passkey löschen",
|
|
||||||
"s_nfc_dialog_fido_delete_fingerprint": "Aktion: Fingerabdruck löschen",
|
|
||||||
"s_nfc_dialog_fido_rename_fingerprint": "Aktion: Fingerabdruck umbenennen",
|
|
||||||
"s_nfc_dialog_fido_failure": "FIDO-Operation fehlgeschlagen",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "Bereit zum Scannen",
|
"s_nfc_ready_to_scan": "Bereit zum Scannen",
|
||||||
"s_nfc_hold_still": "Stillhalten\u2026",
|
"s_nfc_hold_still": "Stillhalten\u2026",
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
|
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
|
||||||
"s_allow_screenshots": "Allow screenshots",
|
"s_allow_screenshots": "Allow screenshots",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "Tap and hold your key",
|
|
||||||
"s_nfc_dialog_operation_success": "Success",
|
|
||||||
"s_nfc_dialog_operation_failed": "Failed",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "Action: reset OATH application",
|
|
||||||
"s_nfc_dialog_oath_unlock": "Action: unlock OATH application",
|
|
||||||
"s_nfc_dialog_oath_set_password": "Action: set OATH password",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "Action: remove OATH password",
|
|
||||||
"s_nfc_dialog_oath_add_account": "Action: add new account",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "Action: rename account",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "Action: delete account",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code",
|
|
||||||
"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 application",
|
|
||||||
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO application",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "Action: set or change the FIDO 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",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "Ready to scan",
|
"s_nfc_ready_to_scan": "Ready to scan",
|
||||||
"s_nfc_hold_still": "Hold still\u2026",
|
"s_nfc_hold_still": "Hold still\u2026",
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB",
|
"l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB",
|
||||||
"s_allow_screenshots": "Autoriser captures d'écran",
|
"s_allow_screenshots": "Autoriser captures d'écran",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "Appuyez et maintenez votre clé",
|
|
||||||
"s_nfc_dialog_operation_success": "Succès",
|
|
||||||
"s_nfc_dialog_operation_failed": "Échec",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "Action\u00a0: réinitialiser applet OATH",
|
|
||||||
"s_nfc_dialog_oath_unlock": "Action\u00a0: débloquer applet OATH",
|
|
||||||
"s_nfc_dialog_oath_set_password": "Action\u00a0: définir mot de passe OATH",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "Action\u00a0: supprimer mot de passe OATH",
|
|
||||||
"s_nfc_dialog_oath_add_account": "Action\u00a0: ajouter nouveau compte",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "Action\u00a0: renommer compte",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "Action\u00a0: supprimer compte",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "Action\u00a0: calculer code OATH",
|
|
||||||
"s_nfc_dialog_oath_failure": "Opération OATH impossible",
|
|
||||||
"s_nfc_dialog_oath_add_multiple_accounts": "Action\u00a0: ajouter plusieurs comptes",
|
|
||||||
|
|
||||||
"s_nfc_dialog_fido_reset": "Action : réinitialiser l'application FIDO",
|
|
||||||
"s_nfc_dialog_fido_unlock": "Action : déverrouiller l'application FIDO",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "Action : définir ou modifier le code PIN FIDO",
|
|
||||||
"s_nfc_dialog_fido_delete_credential": "Action : supprimer le Passkey",
|
|
||||||
"s_nfc_dialog_fido_delete_fingerprint": "Action : supprimer l'empreinte digitale",
|
|
||||||
"s_nfc_dialog_fido_rename_fingerprint": "Action : renommer l'empreinte digitale",
|
|
||||||
"s_nfc_dialog_fido_failure": "Échec de l'opération FIDO",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "Prêt à numériser",
|
"s_nfc_ready_to_scan": "Prêt à numériser",
|
||||||
"s_nfc_hold_still": "Ne bougez pas\u2026",
|
"s_nfc_hold_still": "Ne bougez pas\u2026",
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます",
|
"l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます",
|
||||||
"s_allow_screenshots": "スクリーンショットを許可",
|
"s_allow_screenshots": "スクリーンショットを許可",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "キーをタップして長押しします",
|
|
||||||
"s_nfc_dialog_operation_success": "成功",
|
|
||||||
"s_nfc_dialog_operation_failed": "失敗",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "アクション:OATHアプレットをリセット",
|
|
||||||
"s_nfc_dialog_oath_unlock": "アクション:OATHアプレットをロック解除",
|
|
||||||
"s_nfc_dialog_oath_set_password": "アクション:OATHパスワードを設定",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "アクション:OATHパスワードを削除",
|
|
||||||
"s_nfc_dialog_oath_add_account": "アクション:新しいアカウントを追加",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "アクション:アカウント名を変更",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "アクション:アカウントを削除",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "アクション:OATHコードを計算",
|
|
||||||
"s_nfc_dialog_oath_failure": "OATH操作が失敗しました",
|
|
||||||
"s_nfc_dialog_oath_add_multiple_accounts": "アクション:複数アカウントを追加",
|
|
||||||
|
|
||||||
"s_nfc_dialog_fido_reset": "アクション: FIDOアプリケーションをリセット",
|
|
||||||
"s_nfc_dialog_fido_unlock": "アクション:FIDOアプリケーションのロックを解除する",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "アクション:FIDOのPINの設定または変更",
|
|
||||||
"s_nfc_dialog_fido_delete_credential": "アクション: パスキーを削除",
|
|
||||||
"s_nfc_dialog_fido_delete_fingerprint": "アクション: 指紋の削除",
|
|
||||||
"s_nfc_dialog_fido_rename_fingerprint": "アクション: 指紋の名前を変更する",
|
|
||||||
"s_nfc_dialog_fido_failure": "FIDO操作に失敗しました",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "スキャン準備完了",
|
"s_nfc_ready_to_scan": "スキャン準備完了",
|
||||||
"s_nfc_hold_still": "そのまま\u2026",
|
"s_nfc_hold_still": "そのまま\u2026",
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z klucza YubiKey przez USB",
|
"l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z klucza YubiKey przez USB",
|
||||||
"s_allow_screenshots": "Zezwalaj na zrzuty ekranu",
|
"s_allow_screenshots": "Zezwalaj na zrzuty ekranu",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "Zbliż i przytrzymaj klucz",
|
|
||||||
"s_nfc_dialog_operation_success": "Udało się",
|
|
||||||
"s_nfc_dialog_operation_failed": "Niepowodzenie",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "Działanie: zresetowanie aplikacji OATH",
|
|
||||||
"s_nfc_dialog_oath_unlock": "Działanie: odblokowanie aplikacji OATH",
|
|
||||||
"s_nfc_dialog_oath_set_password": "Działanie: ustawienie hasła OATH",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "Działanie: usunięcie hasła OATH",
|
|
||||||
"s_nfc_dialog_oath_add_account": "Działanie: dodanie nowego konta",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "Działanie: zmiana nazwy konta",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "Działanie: usunięcie konta",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "Działanie: obliczenie kodu OATH",
|
|
||||||
"s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się",
|
|
||||||
"s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodanie wielu kont",
|
|
||||||
|
|
||||||
"s_nfc_dialog_fido_reset": "Działanie: zresetowanie aplikacji FIDO",
|
|
||||||
"s_nfc_dialog_fido_unlock": "Działanie: odblokowanie aplikacji FIDO",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "Działanie: ustawienie lub zmiana kodu PIN FIDO",
|
|
||||||
"s_nfc_dialog_fido_delete_credential": "Działanie: usunięcie klucza dostępu",
|
|
||||||
"s_nfc_dialog_fido_delete_fingerprint": "Działanie: usunięcie odcisku palca",
|
|
||||||
"s_nfc_dialog_fido_rename_fingerprint": "Działanie: zmiana nazwy odcisku palca",
|
|
||||||
"s_nfc_dialog_fido_failure": "Operacja FIDO nie powiodła się",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "Gotowy do skanowania",
|
"s_nfc_ready_to_scan": "Gotowy do skanowania",
|
||||||
"s_nfc_hold_still": "Nie ruszaj się\u2026",
|
"s_nfc_hold_still": "Nie ruszaj się\u2026",
|
||||||
|
@ -899,29 +899,6 @@
|
|||||||
"l_launch_app_on_usb_off": "Các ứng dụng khác có thể sử dụng YubiKey qua USB",
|
"l_launch_app_on_usb_off": "Các ứng dụng khác có thể sử dụng YubiKey qua USB",
|
||||||
"s_allow_screenshots": "Cho phép chụp ảnh màn hình",
|
"s_allow_screenshots": "Cho phép chụp ảnh màn hình",
|
||||||
|
|
||||||
"l_nfc_dialog_tap_key": "Chạm và giữ khóa của bạn",
|
|
||||||
"s_nfc_dialog_operation_success": "Thành công",
|
|
||||||
"s_nfc_dialog_operation_failed": "Thất bại",
|
|
||||||
|
|
||||||
"s_nfc_dialog_oath_reset": "Hành động: đặt lại ứng dụng OATH",
|
|
||||||
"s_nfc_dialog_oath_unlock": "Hành động: mở khóa ứng dụng OATH",
|
|
||||||
"s_nfc_dialog_oath_set_password": "Hành động: đặt mật khẩu OATH",
|
|
||||||
"s_nfc_dialog_oath_unset_password": "Hành động: xóa mật khẩu OATH",
|
|
||||||
"s_nfc_dialog_oath_add_account": "Hành động: thêm tài khoản mới",
|
|
||||||
"s_nfc_dialog_oath_rename_account": "Hành động: đổi tên tài khoản",
|
|
||||||
"s_nfc_dialog_oath_delete_account": "Hành động: xóa tài khoản",
|
|
||||||
"s_nfc_dialog_oath_calculate_code": "Hành động: tính toán mã OATH",
|
|
||||||
"s_nfc_dialog_oath_failure": "Hành động OATH thất bại",
|
|
||||||
"s_nfc_dialog_oath_add_multiple_accounts": "Hành động: thêm nhiều tài khoản",
|
|
||||||
|
|
||||||
"s_nfc_dialog_fido_reset": "Hành động: đặt lại ứng dụng FIDO",
|
|
||||||
"s_nfc_dialog_fido_unlock": "Hành động: mở khóa ứng dụng FIDO",
|
|
||||||
"l_nfc_dialog_fido_set_pin": "Hành động: đặt hoặc thay đổi PIN FIDO",
|
|
||||||
"s_nfc_dialog_fido_delete_credential": "Hành động: xóa Passkey",
|
|
||||||
"s_nfc_dialog_fido_delete_fingerprint": "Hành động: xóa dấu vân tay",
|
|
||||||
"s_nfc_dialog_fido_rename_fingerprint": "Hành động: đổi tên dấu vân tay",
|
|
||||||
"s_nfc_dialog_fido_failure": "Hành động FIDO thất bại",
|
|
||||||
|
|
||||||
"@_nfc": {},
|
"@_nfc": {},
|
||||||
"s_nfc_ready_to_scan": "Sẵn sàng để quét",
|
"s_nfc_ready_to_scan": "Sẵn sàng để quét",
|
||||||
"s_nfc_hold_still": "Giữ yên\u2026",
|
"s_nfc_hold_still": "Giữ yên\u2026",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022-2023 Yubico.
|
* Copyright (C) 2022-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -40,7 +40,6 @@ import '../../widgets/app_text_field.dart';
|
|||||||
import '../../widgets/choice_filter_chip.dart';
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
import '../../widgets/file_drop_overlay.dart';
|
import '../../widgets/file_drop_overlay.dart';
|
||||||
import '../../widgets/file_drop_target.dart';
|
import '../../widgets/file_drop_target.dart';
|
||||||
import '../../widgets/focus_utils.dart';
|
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../../widgets/utf8_utils.dart';
|
import '../../widgets/utf8_utils.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -74,6 +73,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
final _issuerController = TextEditingController();
|
final _issuerController = TextEditingController();
|
||||||
final _accountController = TextEditingController();
|
final _accountController = TextEditingController();
|
||||||
final _secretController = TextEditingController();
|
final _secretController = TextEditingController();
|
||||||
|
final _issuerFocus = FocusNode();
|
||||||
|
final _accountFocus = FocusNode();
|
||||||
|
final _secretFocus = FocusNode();
|
||||||
final _periodController = TextEditingController(text: '$defaultPeriod');
|
final _periodController = TextEditingController(text: '$defaultPeriod');
|
||||||
UserInteractionController? _promptController;
|
UserInteractionController? _promptController;
|
||||||
Uri? _otpauthUri;
|
Uri? _otpauthUri;
|
||||||
@ -88,6 +90,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
List<int> _periodValues = [20, 30, 45, 60];
|
List<int> _periodValues = [20, 30, 45, 60];
|
||||||
List<int> _digitsValues = [6, 8];
|
List<int> _digitsValues = [6, 8];
|
||||||
List<OathCredential>? _credentials;
|
List<OathCredential>? _credentials;
|
||||||
|
bool _submitting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -95,6 +98,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
_accountController.dispose();
|
_accountController.dispose();
|
||||||
_secretController.dispose();
|
_secretController.dispose();
|
||||||
_periodController.dispose();
|
_periodController.dispose();
|
||||||
|
_issuerFocus.dispose();
|
||||||
|
_accountFocus.dispose();
|
||||||
|
_secretFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +127,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
_counter = data.counter;
|
_counter = data.counter;
|
||||||
_isObscure = true;
|
_isObscure = true;
|
||||||
_dataLoaded = true;
|
_dataLoaded = true;
|
||||||
|
_submitting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,8 +135,6 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
{DevicePath? devicePath, required Uri credUri}) async {
|
{DevicePath? devicePath, required Uri credUri}) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
try {
|
try {
|
||||||
FocusUtils.unfocus(context);
|
|
||||||
|
|
||||||
if (devicePath == null) {
|
if (devicePath == null) {
|
||||||
assert(isAndroid, 'devicePath is only optional for Android');
|
assert(isAndroid, 'devicePath is only optional for Android');
|
||||||
await ref
|
await ref
|
||||||
@ -272,6 +277,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
|
|
||||||
void submit() async {
|
void submit() async {
|
||||||
if (secretLengthValid && secretFormatValid) {
|
if (secretLengthValid && secretFormatValid) {
|
||||||
|
_issuerFocus.unfocus();
|
||||||
|
_accountFocus.unfocus();
|
||||||
|
_secretFocus.unfocus();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_submitting = true;
|
||||||
|
});
|
||||||
|
|
||||||
final cred = CredentialData(
|
final cred = CredentialData(
|
||||||
issuer: issuerText.isEmpty ? null : issuerText,
|
issuer: issuerText.isEmpty ? null : issuerText,
|
||||||
name: nameText,
|
name: nameText,
|
||||||
@ -302,6 +315,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_submitting = false;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validateSecret = true;
|
_validateSecret = true;
|
||||||
@ -372,8 +389,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_issuer_optional,
|
labelText: l10n.s_issuer_optional,
|
||||||
helperText:
|
helperText: '', // Prevents dialog resizing when
|
||||||
'', // Prevents dialog resizing when disabled
|
|
||||||
errorText: (byteLength(issuerText) > issuerMaxLength)
|
errorText: (byteLength(issuerText) > issuerMaxLength)
|
||||||
? '' // needs empty string to render as error
|
? '' // needs empty string to render as error
|
||||||
: issuerNoColon
|
: issuerNoColon
|
||||||
@ -382,6 +398,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
prefixIcon: const Icon(Symbols.business),
|
prefixIcon: const Icon(Symbols.business),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
focusNode: _issuerFocus,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// Update maxlengths
|
// Update maxlengths
|
||||||
@ -400,19 +417,22 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_account_name,
|
labelText: l10n.s_account_name,
|
||||||
helperText: '',
|
helperText:
|
||||||
// Prevents dialog resizing when disabled
|
'', // Prevents dialog resizing when disabled
|
||||||
errorText: (byteLength(nameText) > nameMaxLength)
|
errorText: _submitting
|
||||||
? '' // needs empty string to render as error
|
? null
|
||||||
: isUnique
|
: (byteLength(nameText) > nameMaxLength)
|
||||||
? null
|
? '' // needs empty string to render as error
|
||||||
: l10n.l_name_already_exists,
|
: isUnique
|
||||||
|
? null
|
||||||
|
: l10n.l_name_already_exists,
|
||||||
prefixIcon: const Icon(Symbols.person),
|
prefixIcon: const Icon(Symbols.person),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
focusNode: _accountFocus,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// Update maxlengths
|
// Update max lengths
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
@ -452,6 +472,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
)),
|
)),
|
||||||
readOnly: _dataLoaded,
|
readOnly: _dataLoaded,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
|
focusNode: _secretFocus,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validateSecret = false;
|
_validateSecret = false;
|
||||||
|
@ -25,7 +25,6 @@ import '../../app/state.dart';
|
|||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../../widgets/app_input_decoration.dart';
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/focus_utils.dart';
|
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -63,8 +62,14 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _removeFocus() {
|
||||||
|
_currentPasswordFocus.unfocus();
|
||||||
|
_newPasswordFocus.unfocus();
|
||||||
|
_confirmPasswordFocus.unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
FocusUtils.unfocus(context);
|
_removeFocus();
|
||||||
|
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(oathStateProvider(widget.path).notifier)
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
@ -171,6 +176,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
onPressed: _currentPasswordController.text.isNotEmpty &&
|
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||||
!_currentIsWrong
|
!_currentIsWrong
|
||||||
? () async {
|
? () async {
|
||||||
|
_removeFocus();
|
||||||
|
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(oathStateProvider(widget.path).notifier)
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
.unsetPassword(
|
.unsetPassword(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022-2023 Yubico.
|
* Copyright (C) 2022-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -28,7 +28,6 @@ import '../../desktop/models.dart';
|
|||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../widgets/app_input_decoration.dart';
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/focus_utils.dart';
|
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../../widgets/utf8_utils.dart';
|
import '../../widgets/utf8_utils.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -93,7 +92,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
|
|||||||
} on CancellationException catch (_) {
|
} on CancellationException catch (_) {
|
||||||
// ignored
|
// ignored
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.error('Failed to add account', e);
|
_log.error('Failed to rename account', e);
|
||||||
final String errorMessage;
|
final String errorMessage;
|
||||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||||
if (e is RpcError) {
|
if (e is RpcError) {
|
||||||
@ -118,6 +117,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
late String _issuer;
|
late String _issuer;
|
||||||
late String _name;
|
late String _name;
|
||||||
|
|
||||||
|
final _issuerFocus = FocusNode();
|
||||||
|
final _nameFocus = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -125,8 +127,16 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
_name = widget.name.trim();
|
_name = widget.name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_issuerFocus.dispose();
|
||||||
|
_nameFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _submit() async {
|
void _submit() async {
|
||||||
FocusUtils.unfocus(context);
|
_issuerFocus.unfocus();
|
||||||
|
_nameFocus.unfocus();
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
final renamed =
|
final renamed =
|
||||||
await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name);
|
await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name);
|
||||||
@ -188,6 +198,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
prefixIcon: const Icon(Symbols.business),
|
prefixIcon: const Icon(Symbols.business),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
focusNode: _issuerFocus,
|
||||||
|
autofocus: true,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_issuer = value.trim();
|
_issuer = value.trim();
|
||||||
@ -212,6 +224,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
prefixIcon: const Icon(Symbols.people_alt),
|
prefixIcon: const Icon(Symbols.people_alt),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
|
focusNode: _nameFocus,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_name = value.trim();
|
_name = value.trim();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2021-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
const defaultPrimaryColor = Colors.lightGreen;
|
const defaultPrimaryColor = Colors.lightGreen;
|
||||||
|
|
||||||
@ -50,6 +51,9 @@ class AppTheme {
|
|||||||
fontFamily: 'Roboto',
|
fontFamily: 'Roboto',
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness: Brightness.dark,
|
||||||
|
statusBarColor: Colors.transparent),
|
||||||
),
|
),
|
||||||
listTileTheme: const ListTileThemeData(
|
listTileTheme: const ListTileThemeData(
|
||||||
// For alignment under menu button
|
// For alignment under menu button
|
||||||
@ -81,6 +85,9 @@ class AppTheme {
|
|||||||
scaffoldBackgroundColor: colorScheme.surface,
|
scaffoldBackgroundColor: colorScheme.surface,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
statusBarColor: Colors.transparent),
|
||||||
),
|
),
|
||||||
listTileTheme: const ListTileThemeData(
|
listTileTheme: const ListTileThemeData(
|
||||||
// For alignment under menu button
|
// For alignment under menu button
|
||||||
|
Loading…
Reference in New Issue
Block a user