mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
split view model
This commit is contained in:
parent
02b0e331b8
commit
acfc93da31
@ -1,9 +0,0 @@
|
||||
package com.yubico.authenticator.api
|
||||
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
|
||||
class HDialogApiImpl(private val viewModel: MainViewModel) : Pigeon.HDialogApi {
|
||||
override fun dialogClosed(result: Pigeon.Result<Void>) {
|
||||
viewModel.onDialogClosed(result)
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.yubico.authenticator.api.Pigeon
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
|
||||
enum class OperationContext(val value: Long) {
|
||||
Oath(0), Yubikey(1), Invalid(-1);
|
||||
@ -11,18 +13,17 @@ enum class OperationContext(val value: Long) {
|
||||
}
|
||||
}
|
||||
|
||||
class AppContext : Pigeon.AppApi {
|
||||
class AppContext(messenger: BinaryMessenger) : Pigeon.AppApi {
|
||||
private var _appContext = MutableLiveData(OperationContext.Oath)
|
||||
val appContext: LiveData<OperationContext> = _appContext
|
||||
|
||||
private var _operationContext = OperationContext.Oath
|
||||
|
||||
fun getContext() : OperationContext {
|
||||
return _operationContext
|
||||
init {
|
||||
Pigeon.AppApi.setup(messenger, this)
|
||||
}
|
||||
|
||||
override fun setContext(subPageIndex: Long, result: Pigeon.Result<Void>) {
|
||||
_operationContext = OperationContext.getByValue(subPageIndex)
|
||||
Logger.d("Operation context is now $_operationContext")
|
||||
_appContext.value = OperationContext.getByValue(subPageIndex)
|
||||
FlutterLog.d("App context is now $_appContext")
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import com.yubico.authenticator.api.Pigeon.*
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
typealias OnDialogClosed = () -> Unit
|
||||
typealias OnDialogCancelled = () -> Unit
|
||||
|
||||
class DialogManager(messenger: BinaryMessenger, private var coroutineScope: CoroutineScope) :
|
||||
HDialogApi {
|
||||
|
||||
private val _fDialogApi = FDialogApi(messenger)
|
||||
|
||||
private var onCancelled: OnDialogCancelled? = null
|
||||
|
||||
init {
|
||||
HDialogApi.setup(messenger, this)
|
||||
}
|
||||
|
||||
fun showDialog(message: String, cancelled: OnDialogCancelled?) =
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
_fDialogApi.showDialogApi(message) { }
|
||||
}.also {
|
||||
onCancelled = cancelled
|
||||
}
|
||||
|
||||
fun closeDialog(onClosed: OnDialogClosed) {
|
||||
_fDialogApi.closeDialogApi {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
onClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dialogClosed(result: Result<Void>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
onCancelled?.invoke()
|
||||
result.success(null)
|
||||
} catch (cause: Throwable) {
|
||||
FlutterLog.d("Failed to close dialog during User cancel action")
|
||||
result.error(Exception("Failed to close dialog during User cancel action"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,30 +16,37 @@ class FlutterLog(messenger: BinaryMessenger, private val activity: MainActivity)
|
||||
instance = FlutterLog(messenger, activity)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun t(message: String, error: String? = null) {
|
||||
instance.log("t", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun d(message: String, error: String? = null) {
|
||||
instance.log("d", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun i(message: String, error: String? = null) {
|
||||
instance.log("i", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun w(message: String, error: String? = null) {
|
||||
instance.log("w", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun e(message: String, error: String? = null) {
|
||||
instance.log("e", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun wtf(message: String, error: String? = null) {
|
||||
instance.log("wtf", message, error)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun v(message: String, error: String? = null) {
|
||||
instance.log("v", message, error)
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ package com.yubico.authenticator
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.yubico.authenticator.api.HDialogApiImpl
|
||||
import com.yubico.authenticator.oath.OathApiImpl
|
||||
import com.yubico.authenticator.api.Pigeon
|
||||
import com.yubico.authenticator.oath.OathManager
|
||||
import com.yubico.yubikit.android.YubiKitManager
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||
@ -13,9 +11,9 @@ import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
@ -55,42 +53,9 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
yubikit.stopUsbDiscovery()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.yubiKeyDevice.observe(this) { yubikey ->
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (yubikey != null) {
|
||||
Logger.d("A device was connected: $yubikey")
|
||||
viewModel.yubikeyAttached(yubikey)
|
||||
|
||||
} else {
|
||||
Logger.d("A device was disconnected")
|
||||
viewModel.yubikeyDetached()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val appContext = AppContext()
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
viewModel.setAppContext(appContext)
|
||||
viewModel.setFOathApi(Pigeon.FOathApi(messenger))
|
||||
viewModel.setFManagementApi(Pigeon.FManagementApi(messenger))
|
||||
viewModel.setFDialogApi(Pigeon.FDialogApi(messenger))
|
||||
Pigeon.OathApi.setup(messenger, OathApiImpl(viewModel))
|
||||
Pigeon.AppApi.setup(messenger, appContext)
|
||||
Pigeon.HDialogApi.setup(messenger, HDialogApiImpl(viewModel))
|
||||
|
||||
|
||||
// simple logger for yubikit
|
||||
private fun initializeLogger(messenger: BinaryMessenger) {
|
||||
Logger.setLogger(object : Logger() {
|
||||
init {
|
||||
FlutterLog.create(messenger, this@MainActivity)
|
||||
@ -104,6 +69,23 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
FlutterLog.e(message, throwable.message ?: throwable.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private lateinit var appContext: AppContext
|
||||
private lateinit var oathManager: OathManager
|
||||
private lateinit var dialogManager: DialogManager
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
appContext = AppContext(messenger)
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
|
||||
oathManager = OathManager(this, messenger, appContext, viewModel, dialogManager)
|
||||
|
||||
initializeLogger(messenger)
|
||||
|
||||
}
|
||||
|
||||
|
@ -3,558 +3,12 @@ package com.yubico.authenticator
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yubico.authenticator.api.Pigeon
|
||||
import com.yubico.authenticator.data.device.toJson
|
||||
import com.yubico.authenticator.oath.*
|
||||
import com.yubico.authenticator.oath.keystore.ClearingMemProvider
|
||||
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.oath.*
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import kotlinx.coroutines.*
|
||||
import java.lang.IllegalStateException
|
||||
import java.net.URI
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class YubiKeyAction(
|
||||
val message: String,
|
||||
val action: suspend (Result<YubiKeyDevice, Exception>) -> Unit
|
||||
)
|
||||
|
||||
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
||||
private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
private val _handleYubiKey = MutableLiveData(true)
|
||||
val handleYubiKey: LiveData<Boolean> = _handleYubiKey
|
||||
|
||||
val yubiKeyDevice = MutableLiveData<YubiKeyDevice?>()
|
||||
private var _isUsbKey: Boolean = false
|
||||
private var _previousNfcDeviceId = ""
|
||||
|
||||
private val _memoryKeyProvider = ClearingMemProvider()
|
||||
private val _keyManager = KeyManager(KeyStoreProvider(), _memoryKeyProvider)
|
||||
|
||||
|
||||
|
||||
private lateinit var _fOathApi: Pigeon.FOathApi
|
||||
private lateinit var _fManagementApi: Pigeon.FManagementApi
|
||||
private lateinit var _fDialogApi: Pigeon.FDialogApi
|
||||
|
||||
|
||||
private lateinit var _appContext : AppContext
|
||||
|
||||
fun setAppContext(appContext: AppContext) {
|
||||
_appContext = appContext
|
||||
}
|
||||
|
||||
fun setFOathApi(oathApi: Pigeon.FOathApi) {
|
||||
_fOathApi = oathApi
|
||||
}
|
||||
|
||||
fun setFManagementApi(managementApi: Pigeon.FManagementApi) {
|
||||
_fManagementApi = managementApi
|
||||
}
|
||||
|
||||
fun setFDialogApi(dialogApi: Pigeon.FDialogApi) {
|
||||
_fDialogApi = dialogApi
|
||||
}
|
||||
|
||||
private suspend fun sendDeviceInfo(device: YubiKeyDevice) {
|
||||
|
||||
val deviceInfoData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
try {
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val deviceInfo = DeviceUtil.readInfo(result.value, pid)
|
||||
val name = DeviceUtil.getName(deviceInfo, pid?.type)
|
||||
|
||||
val deviceInfoData = deviceInfo
|
||||
.toJson(name, device is NfcYubiKeyDevice)
|
||||
.toString()
|
||||
it.resume(deviceInfoData)
|
||||
} catch (cause: Throwable) {
|
||||
Logger.e("Failed to get device info", cause)
|
||||
it.resumeWithException(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fManagementApi.updateDeviceInfo(deviceInfoData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathInfo(device: YubiKeyDevice) {
|
||||
|
||||
val oathSessionData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val oathSession = OathSession(result.value)
|
||||
|
||||
val deviceId = oathSession.deviceId
|
||||
|
||||
_previousNfcDeviceId = if (device is NfcYubiKeyDevice) {
|
||||
if (deviceId != _previousNfcDeviceId) {
|
||||
// devices are different, clear access key for previous device
|
||||
_memoryKeyProvider.removeKey(_previousNfcDeviceId)
|
||||
}
|
||||
deviceId
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// calling unlock session will remove invalid access keys
|
||||
tryToUnlockOathSession(oathSession)
|
||||
val isRemembered = _keyManager.isRemembered(oathSession.deviceId)
|
||||
|
||||
val oathSessionData = oathSession
|
||||
.toJson(isRemembered)
|
||||
.toString()
|
||||
it.resume(oathSessionData)
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateSession(oathSessionData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathCodes(device: YubiKeyDevice) {
|
||||
val sendOathCodes = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val session = OathSession(result.value)
|
||||
if (tryToUnlockOathSession(session)) {
|
||||
val resultJson = calculateOathCodes(session)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
it.resume(resultJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateOathCredentials(sendOathCodes) {}
|
||||
}
|
||||
|
||||
private val _pendingYubiKeyAction = MutableLiveData<YubiKeyAction?>()
|
||||
private val pendingYubiKeyAction: LiveData<YubiKeyAction?> = _pendingYubiKeyAction
|
||||
|
||||
private suspend fun provideYubiKey(result: Result<YubiKeyDevice, Exception>) =
|
||||
withContext(_dispatcher) {
|
||||
pendingYubiKeyAction.value?.let {
|
||||
_pendingYubiKeyAction.postValue(null)
|
||||
it.action.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun yubikeyAttached(device: YubiKeyDevice) {
|
||||
|
||||
_isUsbKey = device is UsbYubiKeyDevice
|
||||
|
||||
try {
|
||||
|
||||
withContext(_dispatcher) {
|
||||
if (pendingYubiKeyAction.value != null) {
|
||||
provideYubiKey(Result.success(device))
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
when (_appContext.getContext()) {
|
||||
OperationContext.Oath -> {
|
||||
sendDeviceInfo(device)
|
||||
sendOathInfo(device)
|
||||
sendOathCodes(device)
|
||||
}
|
||||
OperationContext.Yubikey -> {
|
||||
sendDeviceInfo(device)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (illegalStateException: IllegalStateException) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
fun yubikeyDetached() {
|
||||
if (_isUsbKey) {
|
||||
// clear keys from memory
|
||||
_memoryKeyProvider.clearAll()
|
||||
_pendingYubiKeyAction.postValue(null)
|
||||
_fManagementApi.updateDeviceInfo("") {}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDialogClosed(result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
provideYubiKey(Result.failure(Exception("User canceled")))
|
||||
result.success(null)
|
||||
} catch (cause: Throwable) {
|
||||
Logger.d("failed")
|
||||
result.error(Exception("Failed to close dialog during User cancel action"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// requests flutter to show a dialog
|
||||
private fun requestShowDialog(message: String) =
|
||||
_fDialogApi.showDialogApi(message) { }
|
||||
|
||||
private fun <T> withUnlockedSession(session: OathSession, block: (OathSession) -> T): T {
|
||||
if (!tryToUnlockOathSession(session)) {
|
||||
throw Exception("Session is locked")
|
||||
}
|
||||
return block(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Steam code or standard TOTP code based on the credential.
|
||||
* @param session OathSession which calculates the TOTP code
|
||||
* @param credential
|
||||
* @param timestamp time for TOTP calculation
|
||||
*
|
||||
* @return calculated Code
|
||||
*/
|
||||
private fun calculateCode(
|
||||
session: OathSession,
|
||||
credential: Credential,
|
||||
timestamp: Long
|
||||
) =
|
||||
if (credential.isSteamCredential()) {
|
||||
session.calculateSteamCode(credential, timestamp)
|
||||
} else {
|
||||
session.calculateCode(credential, timestamp)
|
||||
}
|
||||
|
||||
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
||||
oathSession.credentials.firstOrNull { credential ->
|
||||
(credential != null) && credential.idAsString() == credentialId
|
||||
} ?: throw Exception("Failed to find account to delete")
|
||||
|
||||
|
||||
fun deleteAccount(credentialId: String, result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
useOathSession("Delete account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
session.deleteCredential(credential)
|
||||
returnSuccess(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccount(otpUri: String, requireTouch: Boolean, result: Pigeon.Result<String>) {
|
||||
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Add account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credentialData: CredentialData =
|
||||
CredentialData.parseUri(URI.create(otpUri))
|
||||
|
||||
val credential = session.putCredential(credentialData, requireTouch)
|
||||
|
||||
val code =
|
||||
if (credentialData.oathType == OathType.TOTP && !requireTouch) {
|
||||
// recalculate the code
|
||||
calculateCode(session, credential, System.currentTimeMillis())
|
||||
} else null
|
||||
|
||||
val jsonResult = Pair<Credential, Code?>(credential, code)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, jsonResult)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCredential(
|
||||
credentialId: String,
|
||||
name: String,
|
||||
issuer: String?,
|
||||
result: Pigeon.Result<String>
|
||||
) {
|
||||
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Rename", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
|
||||
val jsonResult =
|
||||
session.renameCredential(credential, name, issuer)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, jsonResult)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setOathPassword(current: String?, password: String, result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Set password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
if (current == null) {
|
||||
throw Exception("Must provide current password to be able to change it")
|
||||
}
|
||||
// test current password sent by the user
|
||||
if (!session.unlock(current.toCharArray())) {
|
||||
throw Exception("Provided current password is invalid")
|
||||
}
|
||||
}
|
||||
val accessKey = session.deriveAccessKey(password.toCharArray())
|
||||
session.setAccessKey(accessKey)
|
||||
_keyManager.addKey(session.deviceId, accessKey, false)
|
||||
Logger.d("Successfully set password")
|
||||
returnSuccess(result)
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun unsetOathPassword(currentPassword: String, result: Pigeon.Result<Void>) {
|
||||
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Unset password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
// test current password sent by the user
|
||||
if (session.unlock(currentPassword.toCharArray())) {
|
||||
session.deleteAccessKey()
|
||||
_keyManager.removeKey(session.deviceId)
|
||||
Logger.d("Successfully unset password")
|
||||
returnSuccess(result)
|
||||
return@useOathSession
|
||||
}
|
||||
}
|
||||
returnError(result, Exception("Unset password failed"))
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateOathCodes(session: OathSession): Map<Credential, Code> {
|
||||
val timeStamp = System.currentTimeMillis()
|
||||
return session.calculateCodes(timeStamp).map { (credential, code) ->
|
||||
Pair(credential, if (credential.isSteamCredential()) {
|
||||
session.calculateSteamCode(credential, timeStamp)
|
||||
} else {
|
||||
code
|
||||
})
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun refreshOathCodes(result: Pigeon.Result<String>) {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
if (!_isUsbKey) {
|
||||
throw Exception("Cannot refresh for nfc key")
|
||||
}
|
||||
|
||||
useOathSession("Refresh codes", false) {
|
||||
withUnlockedSession(it) { session ->
|
||||
val resultJson = calculateOathCodes(session)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
returnSuccess(result, resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun calculate(credentialId: String, result: Pigeon.Result<String>) {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Calculate", true) {
|
||||
withUnlockedSession(it) { session ->
|
||||
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
|
||||
val resultJson = calculateCode(session, credential, System.currentTimeMillis())
|
||||
.toJson()
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unlockOathSession(
|
||||
password: String,
|
||||
remember: Boolean,
|
||||
result: Pigeon.Result<Pigeon.UnlockResponse>
|
||||
) {
|
||||
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
var codes: String? = null
|
||||
useOathSession("Unlocking", true) {
|
||||
val accessKey = it.deriveAccessKey(password.toCharArray())
|
||||
_keyManager.addKey(it.deviceId, accessKey, remember)
|
||||
|
||||
val response = Pigeon.UnlockResponse().apply {
|
||||
isUnlocked = tryToUnlockOathSession(it)
|
||||
isRemembered = _keyManager.isRemembered(it.deviceId)
|
||||
}
|
||||
if (response.isUnlocked == true) {
|
||||
codes = calculateOathCodes(it)
|
||||
.toJson(it.deviceId)
|
||||
.toString()
|
||||
}
|
||||
returnSuccess(result, response)
|
||||
}
|
||||
|
||||
codes?.let {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
_fOathApi.updateOathCredentials(it) {}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetOathSession(result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
try {
|
||||
useOathSession("Reset YubiKey", true) {
|
||||
// note, it is ok to reset locked session
|
||||
it.reset()
|
||||
_keyManager.removeKey(it.deviceId)
|
||||
returnSuccess(result)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
returnError(result, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSession(
|
||||
title: String,
|
||||
queryUserToTap: Boolean,
|
||||
action: (OathSession) -> T
|
||||
) = suspendCoroutine<T> { outer ->
|
||||
if (queryUserToTap && !_isUsbKey) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
requestShowDialog(title)
|
||||
}
|
||||
}
|
||||
_pendingYubiKeyAction.postValue(YubiKeyAction(title) { yubiKey ->
|
||||
outer.resumeWith(runCatching {
|
||||
suspendCoroutine { inner ->
|
||||
yubiKey.value.requestConnection(SmartCardConnection::class.java) {
|
||||
inner.resumeWith(runCatching {
|
||||
action.invoke(OathSession(it.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
yubiKeyDevice.value?.let {
|
||||
viewModelScope.launch(_dispatcher) {
|
||||
provideYubiKey(Result.success(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to unlocks [OathSession] with [AccessKey] stored in [KeyManager]. On failure clears
|
||||
* relevant access keys from [KeyManager]
|
||||
*
|
||||
* @return true if we the session is not locked or it was successfully unlocked, false otherwise
|
||||
*/
|
||||
private fun tryToUnlockOathSession(session: OathSession): Boolean {
|
||||
if (!session.isLocked) {
|
||||
return true
|
||||
}
|
||||
|
||||
val deviceId = session.deviceId
|
||||
val accessKey = _keyManager.getKey(deviceId)
|
||||
?: return false // we have no access key to unlock the session
|
||||
|
||||
val unlockSucceed = session.unlock(accessKey)
|
||||
|
||||
if (unlockSucceed) {
|
||||
return true
|
||||
}
|
||||
|
||||
_keyManager.removeKey(deviceId) // remove invalid access keys from [KeyManager]
|
||||
return false // the unlock did not work, session is locked
|
||||
}
|
||||
|
||||
fun forgetPassword(result: Pigeon.Result<Void>) {
|
||||
_keyManager.clearAll()
|
||||
Logger.d("Cleared all keys.")
|
||||
returnSuccess(result)
|
||||
}
|
||||
|
||||
|
||||
/// for nfc connection waits for the dialog to be closed and then returns success data
|
||||
/// for usb connection returns success data directly
|
||||
private fun <T> returnSuccess(result: Pigeon.Result<T>, data: T? = null) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
if (!_isUsbKey) {
|
||||
_fDialogApi.closeDialogApi {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
result.success(data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.success(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// for nfc connection waits for the dialog to be closed and then returns error
|
||||
/// for usb connection returns error directly
|
||||
private fun <T> returnError(result: Pigeon.Result<T>, error: Throwable) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
if (!_isUsbKey) {
|
||||
_fDialogApi.closeDialogApi {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
result.error(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
|
||||
data class YubiKeyAction(
|
||||
val message: String,
|
||||
val action: suspend (Result<YubiKeyDevice, Exception>) -> Unit
|
||||
)
|
@ -1,62 +0,0 @@
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.api.Pigeon.OathApi
|
||||
import com.yubico.authenticator.api.Pigeon.Result
|
||||
import com.yubico.authenticator.api.Pigeon.UnlockResponse
|
||||
|
||||
class OathApiImpl(private val viewModel: MainViewModel) : OathApi {
|
||||
|
||||
override fun reset(result: Result<Void>) {
|
||||
viewModel.resetOathSession(result)
|
||||
}
|
||||
|
||||
override fun unlock(
|
||||
password: String,
|
||||
remember: Boolean,
|
||||
result: Result<UnlockResponse>
|
||||
) {
|
||||
viewModel.unlockOathSession(password, remember, result)
|
||||
}
|
||||
|
||||
override fun setPassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String,
|
||||
result: Result<Void>
|
||||
) {
|
||||
viewModel.setOathPassword(currentPassword, newPassword, result)
|
||||
}
|
||||
|
||||
override fun unsetPassword(currentPassword: String, result: Result<Void>) {
|
||||
viewModel.unsetOathPassword(currentPassword, result)
|
||||
}
|
||||
|
||||
override fun forgetPassword(result: Result<Void>) {
|
||||
viewModel.forgetPassword(result)
|
||||
}
|
||||
|
||||
override fun addAccount(
|
||||
uri: String,
|
||||
requireTouch: Boolean,
|
||||
result: Result<String>
|
||||
) {
|
||||
viewModel.addAccount(uri, requireTouch, result)
|
||||
}
|
||||
|
||||
override fun renameAccount(uri: String, name: String, issuer: String?, result: Result<String>) {
|
||||
viewModel.renameCredential(uri, name, issuer, result)
|
||||
}
|
||||
|
||||
override fun deleteAccount(uri: String, result: Result<Void>) {
|
||||
viewModel.deleteAccount(uri, result)
|
||||
}
|
||||
|
||||
override fun refreshCodes(result: Result<String>) {
|
||||
viewModel.refreshOathCodes(result)
|
||||
}
|
||||
|
||||
override fun calculate(uri: String, result: Result<String>) {
|
||||
viewModel.calculate(uri, result)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,537 @@
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.*
|
||||
import com.yubico.authenticator.api.Pigeon.*
|
||||
import com.yubico.authenticator.data.device.toJson
|
||||
import com.yubico.authenticator.oath.keystore.ClearingMemProvider
|
||||
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.oath.*
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.URI
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class OathManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
messenger: BinaryMessenger,
|
||||
appContext: AppContext,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val dialogManager: DialogManager
|
||||
) : OathApi {
|
||||
|
||||
private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + _dispatcher)
|
||||
|
||||
private val _fOathApi: FOathApi = FOathApi(messenger)
|
||||
private val _fManagementApi: FManagementApi = FManagementApi(messenger)
|
||||
|
||||
private val _memoryKeyProvider = ClearingMemProvider()
|
||||
private val _keyManager = KeyManager(KeyStoreProvider(), _memoryKeyProvider)
|
||||
private var _previousNfcDeviceId = ""
|
||||
|
||||
private val _pendingYubiKeyAction = MutableLiveData<YubiKeyAction?>()
|
||||
private val pendingYubiKeyAction: LiveData<YubiKeyAction?> = _pendingYubiKeyAction
|
||||
|
||||
init {
|
||||
OathApi.setup(messenger, this)
|
||||
|
||||
appContext.appContext.observe(lifecycleOwner) {
|
||||
if (it == OperationContext.Oath) {
|
||||
installObservers()
|
||||
} else {
|
||||
uninstallObservers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val deviceObserver =
|
||||
Observer<YubiKeyDevice?> { yubiKeyDevice ->
|
||||
if (yubiKeyDevice != null) {
|
||||
yubikeyAttached(yubiKeyDevice)
|
||||
} else {
|
||||
yubikeyDetached()
|
||||
}
|
||||
}
|
||||
|
||||
private fun installObservers() {
|
||||
FlutterLog.d("Installed oath observers")
|
||||
appViewModel.yubiKeyDevice.observe(lifecycleOwner, deviceObserver)
|
||||
}
|
||||
|
||||
private fun uninstallObservers() {
|
||||
appViewModel.yubiKeyDevice.removeObserver(deviceObserver)
|
||||
FlutterLog.d("Uninstalled oath observers")
|
||||
}
|
||||
|
||||
private suspend fun provideYubiKey(result: com.yubico.yubikit.core.util.Result<YubiKeyDevice, Exception>) =
|
||||
pendingYubiKeyAction.value?.let {
|
||||
_pendingYubiKeyAction.postValue(null)
|
||||
it.action.invoke(result)
|
||||
}
|
||||
|
||||
private var _isUsbKey = false
|
||||
private fun yubikeyAttached(device: YubiKeyDevice) {
|
||||
FlutterLog.d("Device connected")
|
||||
|
||||
_isUsbKey = device is UsbYubiKeyDevice
|
||||
|
||||
try {
|
||||
coroutineScope.launch {
|
||||
if (pendingYubiKeyAction.value != null) {
|
||||
provideYubiKey(com.yubico.yubikit.core.util.Result.success(device))
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
sendDeviceInfo(device)
|
||||
sendOathInfo(device)
|
||||
sendOathCodes(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (illegalStateException: IllegalStateException) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private fun yubikeyDetached() {
|
||||
if (_isUsbKey) {
|
||||
FlutterLog.d("Device disconnected")
|
||||
// clear keys from memory
|
||||
_memoryKeyProvider.clearAll()
|
||||
_pendingYubiKeyAction.postValue(null)
|
||||
_fManagementApi.updateDeviceInfo("") {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset(result: Result<Void>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Reset YubiKey", true) {
|
||||
// note, it is ok to reset locked session
|
||||
it.reset()
|
||||
_keyManager.removeKey(it.deviceId)
|
||||
returnSuccess(result)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
returnError(result, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unlock(
|
||||
password: String,
|
||||
remember: Boolean,
|
||||
result: Result<UnlockResponse>
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
var codes: String? = null
|
||||
useOathSession("Unlocking", true) {
|
||||
val accessKey = it.deriveAccessKey(password.toCharArray())
|
||||
_keyManager.addKey(it.deviceId, accessKey, remember)
|
||||
|
||||
val response = UnlockResponse().apply {
|
||||
isUnlocked = tryToUnlockOathSession(it)
|
||||
isRemembered = _keyManager.isRemembered(it.deviceId)
|
||||
}
|
||||
if (response.isUnlocked == true) {
|
||||
codes = calculateOathCodes(it)
|
||||
.toJson(it.deviceId)
|
||||
.toString()
|
||||
}
|
||||
returnSuccess(result, response)
|
||||
}
|
||||
|
||||
codes?.let {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
_fOathApi.updateOathCredentials(it) {}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String,
|
||||
result: Result<Void>
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Set password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
if (currentPassword == null) {
|
||||
throw Exception("Must provide current password to be able to change it")
|
||||
}
|
||||
// test current password sent by the user
|
||||
if (!session.unlock(currentPassword.toCharArray())) {
|
||||
throw Exception("Provided current password is invalid")
|
||||
}
|
||||
}
|
||||
val accessKey = session.deriveAccessKey(newPassword.toCharArray())
|
||||
session.setAccessKey(accessKey)
|
||||
_keyManager.addKey(session.deviceId, accessKey, false)
|
||||
Logger.d("Successfully set password")
|
||||
returnSuccess(result)
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsetPassword(currentPassword: String, result: Result<Void>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Unset password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
// test current password sent by the user
|
||||
if (session.unlock(currentPassword.toCharArray())) {
|
||||
session.deleteAccessKey()
|
||||
_keyManager.removeKey(session.deviceId)
|
||||
Logger.d("Successfully unset password")
|
||||
returnSuccess(result)
|
||||
return@useOathSession
|
||||
}
|
||||
}
|
||||
returnError(result, Exception("Unset password failed"))
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun forgetPassword(result: Result<Void>) {
|
||||
_keyManager.clearAll()
|
||||
Logger.d("Cleared all keys.")
|
||||
returnSuccess(result)
|
||||
}
|
||||
|
||||
override fun addAccount(
|
||||
uri: String,
|
||||
requireTouch: Boolean,
|
||||
result: Result<String>
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Add account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credentialData: CredentialData =
|
||||
CredentialData.parseUri(URI.create(uri))
|
||||
|
||||
val credential = session.putCredential(credentialData, requireTouch)
|
||||
|
||||
val code =
|
||||
if (credentialData.oathType == OathType.TOTP && !requireTouch) {
|
||||
// recalculate the code
|
||||
calculateCode(session, credential, System.currentTimeMillis())
|
||||
} else null
|
||||
|
||||
val jsonResult = Pair<Credential, Code?>(credential, code)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, jsonResult)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun renameAccount(uri: String, name: String, issuer: String?, result: Result<String>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Rename", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, uri)
|
||||
|
||||
val jsonResult =
|
||||
session.renameCredential(credential, name, issuer)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, jsonResult)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAccount(uri: String, result: Result<Void>) {
|
||||
coroutineScope.launch {
|
||||
useOathSession("Delete account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, uri)
|
||||
session.deleteCredential(credential)
|
||||
returnSuccess(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshCodes(result: Result<String>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
if (!_isUsbKey) {
|
||||
throw Exception("Cannot refresh for nfc key")
|
||||
}
|
||||
|
||||
useOathSession("Refresh codes", false) {
|
||||
withUnlockedSession(it) { session ->
|
||||
val resultJson = calculateOathCodes(session)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
returnSuccess(result, resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun calculate(uri: String, result: Result<String>) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
useOathSession("Calculate", true) {
|
||||
withUnlockedSession(it) { session ->
|
||||
|
||||
val credential = getOathCredential(session, uri)
|
||||
|
||||
val resultJson =
|
||||
calculateCode(session, credential, System.currentTimeMillis())
|
||||
.toJson()
|
||||
.toString()
|
||||
|
||||
returnSuccess(result, resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
returnError(result, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Steam code or standard TOTP code based on the credential.
|
||||
* @param session OathSession which calculates the TOTP code
|
||||
* @param credential
|
||||
* @param timestamp time for TOTP calculation
|
||||
*
|
||||
* @return calculated Code
|
||||
*/
|
||||
private fun calculateCode(
|
||||
session: OathSession,
|
||||
credential: Credential,
|
||||
timestamp: Long
|
||||
) =
|
||||
if (credential.isSteamCredential()) {
|
||||
session.calculateSteamCode(credential, timestamp)
|
||||
} else {
|
||||
session.calculateCode(credential, timestamp)
|
||||
}
|
||||
|
||||
private suspend fun sendDeviceInfo(device: YubiKeyDevice) {
|
||||
|
||||
val deviceInfoData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
try {
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val deviceInfo = DeviceUtil.readInfo(result.value, pid)
|
||||
val name = DeviceUtil.getName(deviceInfo, pid?.type)
|
||||
|
||||
val deviceInfoData = deviceInfo
|
||||
.toJson(name, device is NfcYubiKeyDevice)
|
||||
.toString()
|
||||
it.resume(deviceInfoData)
|
||||
} catch (cause: Throwable) {
|
||||
Logger.e("Failed to get device info", cause)
|
||||
it.resumeWithException(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fManagementApi.updateDeviceInfo(deviceInfoData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathInfo(device: YubiKeyDevice) {
|
||||
|
||||
val oathSessionData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val oathSession = OathSession(result.value)
|
||||
|
||||
val deviceId = oathSession.deviceId
|
||||
|
||||
_previousNfcDeviceId = if (device is NfcYubiKeyDevice) {
|
||||
if (deviceId != _previousNfcDeviceId) {
|
||||
// devices are different, clear access key for previous device
|
||||
_memoryKeyProvider.removeKey(_previousNfcDeviceId)
|
||||
}
|
||||
deviceId
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// calling unlock session will remove invalid access keys
|
||||
tryToUnlockOathSession(oathSession)
|
||||
val isRemembered = _keyManager.isRemembered(oathSession.deviceId)
|
||||
|
||||
val oathSessionData = oathSession
|
||||
.toJson(isRemembered)
|
||||
.toString()
|
||||
it.resume(oathSessionData)
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateSession(oathSessionData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathCodes(device: YubiKeyDevice) {
|
||||
val sendOathCodes = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val session = OathSession(result.value)
|
||||
if (tryToUnlockOathSession(session)) {
|
||||
val resultJson = calculateOathCodes(session)
|
||||
.toJson(session.deviceId)
|
||||
.toString()
|
||||
it.resume(resultJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateOathCredentials(sendOathCodes) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to unlocks [OathSession] with [AccessKey] stored in [KeyManager]. On failure clears
|
||||
* relevant access keys from [KeyManager]
|
||||
*
|
||||
* @return true if we the session is not locked or it was successfully unlocked, false otherwise
|
||||
*/
|
||||
private fun tryToUnlockOathSession(session: OathSession): Boolean {
|
||||
if (!session.isLocked) {
|
||||
return true
|
||||
}
|
||||
|
||||
val deviceId = session.deviceId
|
||||
val accessKey = _keyManager.getKey(deviceId)
|
||||
?: return false // we have no access key to unlock the session
|
||||
|
||||
val unlockSucceed = session.unlock(accessKey)
|
||||
|
||||
if (unlockSucceed) {
|
||||
return true
|
||||
}
|
||||
|
||||
_keyManager.removeKey(deviceId) // remove invalid access keys from [KeyManager]
|
||||
return false // the unlock did not work, session is locked
|
||||
}
|
||||
|
||||
private fun calculateOathCodes(session: OathSession): Map<Credential, Code> {
|
||||
val timeStamp = System.currentTimeMillis()
|
||||
return session.calculateCodes(timeStamp).map { (credential, code) ->
|
||||
Pair(
|
||||
credential, if (credential.isSteamCredential()) {
|
||||
session.calculateSteamCode(credential, timeStamp)
|
||||
} else {
|
||||
code
|
||||
}
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun <T> withUnlockedSession(session: OathSession, block: (OathSession) -> T): T {
|
||||
if (!tryToUnlockOathSession(session)) {
|
||||
throw Exception("Session is locked")
|
||||
}
|
||||
return block(session)
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSession(
|
||||
title: String,
|
||||
queryUserToTap: Boolean,
|
||||
action: (OathSession) -> T
|
||||
) = suspendCoroutine<T> { outer ->
|
||||
if (queryUserToTap && !_isUsbKey) {
|
||||
dialogManager.showDialog(title) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
FlutterLog.d("Cancelled Dialog $title")
|
||||
provideYubiKey(com.yubico.yubikit.core.util.Result.failure(Exception("User canceled")))
|
||||
}
|
||||
}
|
||||
}
|
||||
_pendingYubiKeyAction.postValue(YubiKeyAction(title) { yubiKey ->
|
||||
outer.resumeWith(runCatching {
|
||||
suspendCoroutine { inner ->
|
||||
yubiKey.value.requestConnection(SmartCardConnection::class.java) {
|
||||
inner.resumeWith(runCatching {
|
||||
action.invoke(OathSession(it.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (_isUsbKey) {
|
||||
appViewModel.yubiKeyDevice.value?.let {
|
||||
coroutineScope.launch {
|
||||
provideYubiKey(com.yubico.yubikit.core.util.Result.success(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
||||
oathSession.credentials.firstOrNull { credential ->
|
||||
(credential != null) && credential.idAsString() == credentialId
|
||||
} ?: throw Exception("Failed to find account to delete")
|
||||
|
||||
|
||||
/// for nfc connection waits for the dialog to be closed and then returns success data
|
||||
/// for usb connection returns success data directly
|
||||
private fun <T> returnSuccess(result: Result<T>, data: T? = null) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
if (!_isUsbKey) {
|
||||
dialogManager.closeDialog {
|
||||
result.success(data)
|
||||
}
|
||||
} else {
|
||||
result.success(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// for nfc connection waits for the dialog to be closed and then returns error
|
||||
/// for usb connection returns error directly
|
||||
private fun <T> returnError(result: Result<T>, error: Throwable) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
if (!_isUsbKey) {
|
||||
dialogManager.closeDialog {
|
||||
result.error(error)
|
||||
}
|
||||
} else {
|
||||
result.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user