mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-26 10:33:15 +03:00
Handle AppConextManager more robustly.
This commit is contained in:
parent
e60ded1347
commit
f52fe40b01
@ -1,13 +1,12 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.yubico.authenticator.logging.Log
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, private val appViewModel: MainViewModel) {
|
||||
private val channel = FlutterChannel(messenger, "android.state.appContext")
|
||||
private val channel = MethodChannel(messenger, "android.state.appContext")
|
||||
|
||||
init {
|
||||
channel.setHandler(coroutineScope) { method, args ->
|
||||
@ -18,12 +17,11 @@ class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, pri
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun setContext(subPageIndex: Int): String {
|
||||
val appContext = OperationContext.getByValue(subPageIndex)
|
||||
appViewModel.setContext(appContext)
|
||||
Log.d(TAG, "App context is now ${appContext}")
|
||||
return FlutterChannel.NULL
|
||||
appViewModel.setAppContext(appContext)
|
||||
Log.d(TAG, "App context is now $appContext")
|
||||
return NULL
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -2,6 +2,10 @@ package com.yubico.authenticator
|
||||
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
|
||||
/**
|
||||
* Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app.
|
||||
*/
|
||||
interface AppContextManager {
|
||||
suspend fun processYubiKey(device: YubiKeyDevice)
|
||||
fun dispose()
|
||||
}
|
105
android/app/src/main/kotlin/com/yubico/authenticator/ChannelHelper.kt
Executable file
105
android/app/src/main/kotlin/com/yubico/authenticator/ChannelHelper.kt
Executable file
@ -0,0 +1,105 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.Closeable
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Observes a LiveData value, sending each change to Flutter via an EventChannel.
|
||||
*/
|
||||
inline fun <reified T> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, messenger: BinaryMessenger, channelName: String): Closeable {
|
||||
val channel = EventChannel(messenger, channelName)
|
||||
var sink: EventChannel.EventSink? = null
|
||||
|
||||
channel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
sink = events
|
||||
events.success(value?.let(jsonSerializer::encodeToString) ?: NULL)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
sink = null
|
||||
}
|
||||
})
|
||||
|
||||
val observer = Observer<T> {
|
||||
sink?.success(it?.let(jsonSerializer::encodeToString) ?: NULL)
|
||||
}
|
||||
observe(lifecycleOwner, observer)
|
||||
|
||||
return Closeable {
|
||||
removeObserver(observer)
|
||||
channel.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
|
||||
typealias MethodHandler = suspend (method: String, args: Map<String, Any?>) -> String
|
||||
|
||||
/**
|
||||
* Coroutine-based handing of MethodChannel methods called from Flutter.
|
||||
*/
|
||||
fun MethodChannel.setHandler(scope: CoroutineScope, handler: MethodHandler) {
|
||||
setMethodCallHandler { call, result ->
|
||||
// N.B. Arguments from Flutter are passed as a Map of basic types. We may want to
|
||||
// consider JSON encoding if we need to pass more complex structures.
|
||||
// Return values are always JSON strings.
|
||||
val args = call.arguments<Map<String, Any?>>() ?: mapOf()
|
||||
scope.launch {
|
||||
try {
|
||||
val response = handler.invoke(call.method, args)
|
||||
result.success(response)
|
||||
} catch (notImplemented: NotImplementedError) {
|
||||
result.notImplemented()
|
||||
} catch (error: Throwable) {
|
||||
result.error(
|
||||
error.javaClass.simpleName,
|
||||
error.toString(),
|
||||
"Cause: " + error.cause + ", Stacktrace: " + android.util.Log.getStackTraceString(
|
||||
error
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coroutine-based method invocation to call a Flutter method and get a result.
|
||||
*/
|
||||
suspend fun MethodChannel.invoke(method: String, args: Any?): Any? =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
invokeMethod(
|
||||
method,
|
||||
args,
|
||||
object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
|
||||
override fun error(
|
||||
errorCode: String,
|
||||
errorMessage: String?,
|
||||
errorDetails: Any?
|
||||
) {
|
||||
continuation.resumeWithException(Exception("$errorCode: $errorMessage - $errorDetails"))
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
continuation.resumeWithException(NotImplementedError("Method not implemented: $method"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
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
|
||||
@ -15,12 +16,12 @@ enum class Icon(val value: String) {
|
||||
|
||||
class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
|
||||
private val channel =
|
||||
FlutterChannel(messenger, "com.yubico.authenticator.channel.dialog")
|
||||
MethodChannel(messenger, "com.yubico.authenticator.channel.dialog")
|
||||
|
||||
private var onCancelled: OnDialogCancelled? = null
|
||||
|
||||
init {
|
||||
channel.setHandler(coroutineScope) { method, args ->
|
||||
channel.setHandler(coroutineScope) { method, _ ->
|
||||
when (method) {
|
||||
"cancel" -> dialogClosed()
|
||||
else -> throw NotImplementedError()
|
||||
@ -31,7 +32,7 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
|
||||
fun showDialog(icon: Icon, title: String, description: String, cancelled: OnDialogCancelled?) {
|
||||
onCancelled = cancelled
|
||||
coroutineScope.launch {
|
||||
channel.call(
|
||||
channel.invoke(
|
||||
"show",
|
||||
Json.encodeToString(
|
||||
mapOf(
|
||||
@ -49,7 +50,7 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
|
||||
title: String? = null,
|
||||
description: String? = null
|
||||
) {
|
||||
channel.call(
|
||||
channel.invoke(
|
||||
"state",
|
||||
Json.encodeToString(
|
||||
mapOf(
|
||||
@ -63,7 +64,7 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
|
||||
|
||||
fun closeDialog() {
|
||||
coroutineScope.launch {
|
||||
channel.call("close")
|
||||
channel.invoke("close", NULL)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +75,7 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
return FlutterChannel.NULL
|
||||
return NULL
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,76 +0,0 @@
|
||||
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
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class FlutterChannel(
|
||||
messenger: BinaryMessenger,
|
||||
channel: String,
|
||||
) {
|
||||
companion object {
|
||||
const val NULL = "null"
|
||||
}
|
||||
|
||||
private val platform: MethodChannel = MethodChannel(messenger, channel)
|
||||
|
||||
fun setHandler(
|
||||
scope: CoroutineScope,
|
||||
handler: suspend (method: String, args: Map<String, Any?>) -> String
|
||||
) {
|
||||
platform.setMethodCallHandler { call, result ->
|
||||
// N.B. Arguments from Flutter are passed as a Map of basic types. We may want to
|
||||
// consider JSON encoding if we need to pass more complex structures.
|
||||
// Return values are always JSON strings.
|
||||
val args = call.arguments<Map<String, Any?>>() ?: mapOf()
|
||||
scope.launch {
|
||||
try {
|
||||
val response = handler.invoke(call.method, args)
|
||||
result.success(response)
|
||||
} catch (notImplemented: NotImplementedError) {
|
||||
result.notImplemented()
|
||||
} catch (error: Throwable) {
|
||||
result.error(
|
||||
error.javaClass.simpleName,
|
||||
error.toString(),
|
||||
"Cause: " + error.cause + ", Stacktrace: " + android.util.Log.getStackTraceString(
|
||||
error
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun call(method: String, args: String = NULL): Any? =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
platform.invokeMethod(
|
||||
method,
|
||||
args,
|
||||
object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
|
||||
override fun error(
|
||||
errorCode: String,
|
||||
errorMessage: String?,
|
||||
errorDetails: Any?
|
||||
) {
|
||||
continuation.resumeWithException(Exception("$errorCode: $errorMessage - $errorDetails"))
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
continuation.resumeWithException(NotImplementedError("Method not implemented: $method"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package com.yubico.authenticator.oath
|
||||
package com.yubico.authenticator
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
const val NULL = "null"
|
||||
|
||||
val jsonSerializer = Json {
|
||||
// creates properties for default values
|
||||
encodeDefaults = true
|
||||
|
@ -1,26 +0,0 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.yubico.authenticator.oath.jsonSerializer
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
inline fun <reified T> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, channel: EventChannel) {
|
||||
var sink: EventChannel.EventSink? = null
|
||||
|
||||
channel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
sink = events
|
||||
events.success(value?.let(jsonSerializer::encodeToString) ?: "null")
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
sink = null
|
||||
}
|
||||
})
|
||||
|
||||
observe(lifecycleOwner) {
|
||||
sink?.success(it?.let(jsonSerializer::encodeToString) ?: "null")
|
||||
}
|
||||
}
|
@ -20,10 +20,12 @@ import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.Closeable
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@ -67,11 +69,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
Log.d(TAG, "Starting usb discovery")
|
||||
yubikit.startUsbDiscovery(UsbConfiguration()) { device ->
|
||||
viewModel.setConnectedYubiKey(device)
|
||||
contextManager?.let {
|
||||
lifecycleScope.launch {
|
||||
it.processYubiKey(device)
|
||||
}
|
||||
}
|
||||
processYubiKey(device)
|
||||
}
|
||||
hasNfc = startNfcDiscovery()
|
||||
} else {
|
||||
@ -85,13 +83,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
fun startNfcDiscovery(): Boolean =
|
||||
try {
|
||||
Log.d(TAG, "Starting nfc discovery")
|
||||
yubikit.startNfcDiscovery(nfcConfiguration, this) { device ->
|
||||
contextManager?.let {
|
||||
lifecycleScope.launch {
|
||||
it.processYubiKey(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
yubikit.startNfcDiscovery(nfcConfiguration, this, ::processYubiKey)
|
||||
true
|
||||
} catch (e: NfcNotAvailable) {
|
||||
false
|
||||
@ -145,10 +137,14 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
|
||||
lifecycleScope.launch {
|
||||
contextManager?.processYubiKey(device)
|
||||
device.remove {
|
||||
executor.shutdown()
|
||||
startNfcDiscovery()
|
||||
try {
|
||||
contextManager?.processYubiKey(device)
|
||||
device.remove {
|
||||
executor.shutdown()
|
||||
startNfcDiscovery()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error processing YubiKey in AppContextManager", e.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -156,11 +152,24 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var appContext: AppContext
|
||||
private fun processYubiKey(device: YubiKeyDevice) {
|
||||
contextManager?.let {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
it.processYubiKey(device)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error processing YubiKey in AppContextManager", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var contextManager: AppContextManager? = null
|
||||
private lateinit var appContext: AppContext
|
||||
private lateinit var dialogManager: DialogManager
|
||||
private lateinit var appPreferences: AppPreferences
|
||||
private lateinit var flutterLog: FlutterLog
|
||||
private lateinit var flutterStreams: List<Closeable>
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
@ -172,19 +181,27 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
appPreferences = AppPreferences(this)
|
||||
|
||||
viewModel.deviceInfo.streamTo(this, EventChannel(messenger, "android.devices.deviceInfo"))
|
||||
|
||||
oathViewModel.sessionState.streamTo(this, EventChannel(messenger, "android.oath.sessionState"))
|
||||
oathViewModel.credentials.streamTo(this, EventChannel(messenger, "android.oath.credentials"))
|
||||
flutterStreams = listOf(
|
||||
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
|
||||
oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"),
|
||||
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
|
||||
)
|
||||
|
||||
viewModel.appContext.observe(this) {
|
||||
contextManager?.dispose()
|
||||
contextManager = when(it) {
|
||||
OperationContext.Oath -> OathManager(messenger, viewModel, oathViewModel, dialogManager, appPreferences)
|
||||
OperationContext.Oath -> OathManager(this, messenger, viewModel, oathViewModel, dialogManager, appPreferences)
|
||||
else -> null
|
||||
}
|
||||
viewModel.connectedYubiKey.value?.let(::processYubiKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
flutterStreams.forEach { it.close() }
|
||||
super.cleanUpFlutterEngine(flutterEngine)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "MainActivity"
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class MainViewModel : ViewModel() {
|
||||
|
||||
private var _appContext = MutableLiveData(OperationContext.Oath)
|
||||
val appContext: LiveData<OperationContext> = _appContext
|
||||
fun setContext(appContext: OperationContext) = _appContext.postValue(appContext)
|
||||
fun setAppContext(appContext: OperationContext) = _appContext.postValue(appContext)
|
||||
|
||||
private val _connectedYubiKey = MutableLiveData<UsbYubiKeyDevice?>()
|
||||
val connectedYubiKey: LiveData<UsbYubiKeyDevice?> = _connectedYubiKey
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.*
|
||||
import com.yubico.authenticator.logging.Log
|
||||
import com.yubico.authenticator.management.model
|
||||
@ -16,6 +18,7 @@ import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.oath.*
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.net.URI
|
||||
@ -25,6 +28,7 @@ import kotlin.coroutines.suspendCoroutine
|
||||
typealias OathAction = (Result<OathSession, Exception>) -> Unit
|
||||
|
||||
class OathManager(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
messenger: BinaryMessenger,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val oathViewModel: OathViewModel,
|
||||
@ -38,14 +42,45 @@ class OathManager(
|
||||
private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + _dispatcher)
|
||||
|
||||
private val oathChannel = FlutterChannel(messenger, "android.oath.methods")
|
||||
private val oathChannel = MethodChannel(messenger, "android.oath.methods")
|
||||
|
||||
private val _memoryKeyProvider = ClearingMemProvider()
|
||||
private val _keyManager = KeyManager(KeyStoreProvider(), _memoryKeyProvider)
|
||||
|
||||
private var pendingAction: OathAction? = null
|
||||
private var refreshJob: Job? = null
|
||||
|
||||
private val usbObserver = Observer<UsbYubiKeyDevice?> {
|
||||
refreshJob?.cancel()
|
||||
if (it == null) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val credentialObserver = Observer<List<Model.CredentialWithCode>?> { codes ->
|
||||
refreshJob?.cancel()
|
||||
if(codes != null && appViewModel.connectedYubiKey.value != null) {
|
||||
val expirations = codes
|
||||
.filter { it.credential.oathType == Model.OathType.TOTP && !it.credential.touchRequired }
|
||||
.mapNotNull { it.code?.validTo }
|
||||
if(expirations.isNotEmpty()) {
|
||||
val earliest = expirations.min() * 1000
|
||||
val now = System.currentTimeMillis()
|
||||
refreshJob = coroutineScope.launch {
|
||||
if(earliest > now) {
|
||||
delay(earliest - now)
|
||||
}
|
||||
requestRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
oathViewModel.credentials.observe(lifecycleOwner, credentialObserver)
|
||||
|
||||
// OATH methods callable from Flutter:
|
||||
oathChannel.setHandler(coroutineScope) { method, args ->
|
||||
when (method) {
|
||||
@ -71,12 +106,18 @@ class OathManager(
|
||||
args["issuer"] as String?
|
||||
)
|
||||
"deleteAccount" -> deleteAccount(args["credentialId"] as String)
|
||||
"requestRefresh" -> requestRefresh()
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
oathViewModel.credentials.removeObserver(credentialObserver)
|
||||
oathChannel.setMethodCallHandler(null)
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
try {
|
||||
device.withConnection<SmartCardConnection, Unit> {
|
||||
@ -147,8 +188,6 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
//private var _isUsbKey = false
|
||||
|
||||
private suspend fun reset(): String {
|
||||
useOathSession("Reset YubiKey") {
|
||||
// note, it is ok to reset locked session
|
||||
@ -156,7 +195,7 @@ class OathManager(
|
||||
_keyManager.removeKey(it.deviceId)
|
||||
oathViewModel.setSessionState(it.model(false))
|
||||
}
|
||||
return FlutterChannel.NULL
|
||||
return NULL
|
||||
}
|
||||
|
||||
private suspend fun unlock(password: String, remember: Boolean): String =
|
||||
@ -193,7 +232,7 @@ class OathManager(
|
||||
_keyManager.addKey(session.deviceId, accessKey, false)
|
||||
oathViewModel.setSessionState(session.model(false))
|
||||
Log.d(TAG, "Successfully set password")
|
||||
FlutterChannel.NULL
|
||||
NULL
|
||||
}
|
||||
|
||||
private suspend fun unsetPassword(currentPassword: String): String =
|
||||
@ -205,7 +244,7 @@ class OathManager(
|
||||
_keyManager.removeKey(session.deviceId)
|
||||
oathViewModel.setSessionState(session.model(false))
|
||||
Log.d(TAG, "Successfully unset password")
|
||||
return@useOathSession FlutterChannel.NULL
|
||||
return@useOathSession NULL
|
||||
}
|
||||
}
|
||||
throw Exception("Unset password failed")
|
||||
@ -217,7 +256,7 @@ class OathManager(
|
||||
oathViewModel.sessionState.value?.let {
|
||||
oathViewModel.setSessionState(it.copy(isLocked = it.isAccessKeySet, isRemembered = false))
|
||||
}
|
||||
return FlutterChannel.NULL
|
||||
return NULL
|
||||
}
|
||||
|
||||
private suspend fun addAccount(
|
||||
@ -261,19 +300,17 @@ class OathManager(
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
session.deleteCredential(credential)
|
||||
oathViewModel.removeCredential(credential.model(session.deviceId))
|
||||
FlutterChannel.NULL
|
||||
NULL
|
||||
}
|
||||
|
||||
private suspend fun requestRefresh(): String {
|
||||
private suspend fun requestRefresh() {
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
useOathSessionUsb(it) {
|
||||
oathViewModel.updateCredentials(
|
||||
calculateOathCodes(it).model(it.deviceId)
|
||||
)
|
||||
}
|
||||
return FlutterChannel.NULL
|
||||
}
|
||||
throw throw IllegalStateException("Cannot refresh for nfc key")
|
||||
} ?: throw throw IllegalStateException("Cannot refresh for nfc key")
|
||||
}
|
||||
|
||||
private suspend fun calculate(credentialId: String): String =
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.authenticator.device.Version
|
||||
import com.yubico.authenticator.jsonSerializer
|
||||
import com.yubico.authenticator.oath.OathTestHelper.code
|
||||
import com.yubico.authenticator.oath.OathTestHelper.hotp
|
||||
import com.yubico.authenticator.oath.OathTestHelper.totp
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -116,9 +115,6 @@ final androidCredentialListProvider = StateNotifierProvider.autoDispose
|
||||
ref.watch(withContextProvider),
|
||||
ref.watch(currentDeviceProvider)?.transport == Transport.usb,
|
||||
);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
},
|
||||
);
|
||||
@ -128,7 +124,6 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
||||
final WithContext _withContext;
|
||||
final bool _isUsbAttached;
|
||||
late StreamSubscription _sub;
|
||||
Timer? _timer;
|
||||
|
||||
_AndroidCredentialListNotifier(this._withContext, this._isUsbAttached)
|
||||
: super() {
|
||||
@ -138,22 +133,11 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
||||
? List.unmodifiable(
|
||||
(json as List).map((e) => OathPair.fromJson(e)).toList())
|
||||
: null;
|
||||
_scheduleRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (!_isUsbAttached) return;
|
||||
if (windowState.active) {
|
||||
_scheduleRefresh();
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@ -251,47 +235,4 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
_refresh() async {
|
||||
if (!_isUsbAttached) return;
|
||||
_log.debug('refreshing credentials...');
|
||||
|
||||
try {
|
||||
await _methods.invokeMethod('requestRefresh');
|
||||
} catch (e) {
|
||||
_log.debug('Failure refreshing codes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_scheduleRefresh() {
|
||||
if (!_isUsbAttached) return;
|
||||
|
||||
_timer?.cancel();
|
||||
if (state == null) {
|
||||
_log.debug('No OATH state, refresh immediately');
|
||||
_refresh();
|
||||
} else if (mounted) {
|
||||
final expirations = (state ?? [])
|
||||
.where((pair) =>
|
||||
pair.credential.oathType == OathType.totp &&
|
||||
!pair.credential.touchRequired)
|
||||
.map((e) => e.code)
|
||||
.whereType<OathCode>()
|
||||
.map((e) => e.validTo);
|
||||
if (expirations.isEmpty) {
|
||||
_log.debug('No expirations, no refresh');
|
||||
_timer = null;
|
||||
} else {
|
||||
final earliest = expirations.reduce(min) * 1000;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (earliest < now) {
|
||||
_log.debug('Already expired, refresh immediately');
|
||||
_refresh();
|
||||
} else {
|
||||
_log.debug('Schedule refresh in ${earliest - now}ms');
|
||||
_timer = Timer(Duration(milliseconds: earliest - now), _refresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user