mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge PR #1467
This commit is contained in:
commit
91397d264a
@ -334,7 +334,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
|
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
|
||||||
fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
|
fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
|
||||||
fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"),
|
fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"),
|
||||||
|
fidoViewModel.fingerprints.streamTo(this, messenger, "android.fido.fingerprints"),
|
||||||
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
|
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
|
||||||
|
fidoViewModel.registerFingerprint.streamTo(this, messenger, "android.fido.registerFp"),
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.appContext.observe(this) {
|
viewModel.appContext.observe(this) {
|
||||||
@ -348,7 +350,10 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
// 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 ||
|
||||||
(appContext == OperationContext.Oath && contextManager is OathManager) ||
|
(appContext == OperationContext.Oath && contextManager is OathManager) ||
|
||||||
(appContext == OperationContext.FidoPasskeys && contextManager is FidoManager)
|
(appContext in listOf(
|
||||||
|
OperationContext.FidoPasskeys,
|
||||||
|
OperationContext.FidoFingerprints
|
||||||
|
) && contextManager is FidoManager)
|
||||||
) {
|
) {
|
||||||
// no need to dispose this context
|
// no need to dispose this context
|
||||||
} else {
|
} else {
|
||||||
@ -367,6 +372,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
appPreferences
|
appPreferences
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OperationContext.FidoFingerprints,
|
||||||
OperationContext.FidoPasskeys -> FidoManager(
|
OperationContext.FidoPasskeys -> FidoManager(
|
||||||
messenger,
|
messenger,
|
||||||
deviceManager,
|
deviceManager,
|
||||||
|
@ -23,7 +23,10 @@ enum class FidoActionDescription(private val value: Int) {
|
|||||||
Unlock(1),
|
Unlock(1),
|
||||||
SetPin(2),
|
SetPin(2),
|
||||||
DeleteCredential(3),
|
DeleteCredential(3),
|
||||||
ActionFailure(4);
|
DeleteFingerprint(4),
|
||||||
|
RenameFingerprint(5),
|
||||||
|
RegisterFingerprint(6),
|
||||||
|
ActionFailure(7);
|
||||||
|
|
||||||
val id: Int
|
val id: Int
|
||||||
get() = value + dialogDescriptionFidoIndex
|
get() = value + dialogDescriptionFidoIndex
|
||||||
|
@ -16,16 +16,17 @@
|
|||||||
|
|
||||||
package com.yubico.authenticator.fido
|
package com.yubico.authenticator.fido
|
||||||
|
|
||||||
import android.nfc.TagLostException
|
|
||||||
import com.yubico.authenticator.AppContextManager
|
import com.yubico.authenticator.AppContextManager
|
||||||
import com.yubico.authenticator.DialogManager
|
import com.yubico.authenticator.DialogManager
|
||||||
import com.yubico.authenticator.MainViewModel
|
import com.yubico.authenticator.MainViewModel
|
||||||
|
import com.yubico.authenticator.NULL
|
||||||
import com.yubico.authenticator.asString
|
import com.yubico.authenticator.asString
|
||||||
import com.yubico.authenticator.device.DeviceListener
|
import com.yubico.authenticator.device.DeviceListener
|
||||||
import com.yubico.authenticator.device.DeviceManager
|
import com.yubico.authenticator.device.DeviceManager
|
||||||
import com.yubico.authenticator.device.Info
|
import com.yubico.authenticator.device.Info
|
||||||
import com.yubico.authenticator.device.UnknownDevice
|
import com.yubico.authenticator.device.UnknownDevice
|
||||||
import com.yubico.authenticator.fido.data.FidoCredential
|
import com.yubico.authenticator.fido.data.FidoCredential
|
||||||
|
import com.yubico.authenticator.fido.data.FidoFingerprint
|
||||||
import com.yubico.authenticator.fido.data.Session
|
import com.yubico.authenticator.fido.data.Session
|
||||||
import com.yubico.authenticator.fido.data.SessionInfo
|
import com.yubico.authenticator.fido.data.SessionInfo
|
||||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||||
@ -38,13 +39,17 @@ import com.yubico.yubikit.core.Transport
|
|||||||
import com.yubico.yubikit.core.YubiKeyConnection
|
import com.yubico.yubikit.core.YubiKeyConnection
|
||||||
import com.yubico.yubikit.core.YubiKeyDevice
|
import com.yubico.yubikit.core.YubiKeyDevice
|
||||||
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
|
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
|
||||||
|
import com.yubico.yubikit.core.application.CommandState
|
||||||
import com.yubico.yubikit.core.fido.CtapException
|
import com.yubico.yubikit.core.fido.CtapException
|
||||||
import com.yubico.yubikit.core.fido.FidoConnection
|
import com.yubico.yubikit.core.fido.FidoConnection
|
||||||
|
import com.yubico.yubikit.core.internal.Logger
|
||||||
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 com.yubico.yubikit.fido.ctap.BioEnrollment
|
||||||
import com.yubico.yubikit.fido.ctap.ClientPin
|
import com.yubico.yubikit.fido.ctap.ClientPin
|
||||||
import com.yubico.yubikit.fido.ctap.CredentialManagement
|
import com.yubico.yubikit.fido.ctap.CredentialManagement
|
||||||
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
|
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
|
||||||
|
import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment
|
||||||
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol
|
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol
|
||||||
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol
|
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol
|
||||||
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1
|
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1
|
||||||
@ -72,6 +77,12 @@ class FidoManager(
|
|||||||
dialogManager: DialogManager,
|
dialogManager: DialogManager,
|
||||||
) : AppContextManager(), DeviceListener {
|
) : AppContextManager(), DeviceListener {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private object HexCodec {
|
||||||
|
fun bytesToHexString(bytes: ByteArray) : String = bytes.toHexString()
|
||||||
|
fun hexStringToBytes(hex: String) : ByteArray = hex.hexToByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
|
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
|
||||||
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
|
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
|
||||||
@ -135,6 +146,21 @@ class FidoManager(
|
|||||||
args["credentialId"] as String
|
args["credentialId"] as String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"deleteFingerprint" -> deleteFingerprint(
|
||||||
|
args["templateId"] as String
|
||||||
|
)
|
||||||
|
|
||||||
|
"renameFingerprint" -> renameFingerprint(
|
||||||
|
args["templateId"] as String,
|
||||||
|
args["name"] as String
|
||||||
|
)
|
||||||
|
|
||||||
|
"registerFingerprint" -> registerFingerprint(
|
||||||
|
args["name"] as String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
"cancelRegisterFingerprint" -> cancelRegisterFingerprint()
|
||||||
|
|
||||||
else -> throw NotImplementedError()
|
else -> throw NotImplementedError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,8 +241,7 @@ class FidoManager(
|
|||||||
|
|
||||||
fidoViewModel.setSessionState(
|
fidoViewModel.setSessionState(
|
||||||
Session(
|
Session(
|
||||||
fidoSession.cachedInfo,
|
fidoSession.cachedInfo, pinStore.hasPin()
|
||||||
pinStore.hasPin()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -234,12 +259,14 @@ class FidoManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPermissions(fidoSession: YubiKitFidoSession): Int {
|
private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int {
|
||||||
// TODO: Add bio Enrollment permissions if supported
|
|
||||||
return if (CredentialManagement.isSupported(fidoSession.cachedInfo))
|
return if (CredentialManagement.isSupported(fidoSession.cachedInfo))
|
||||||
ClientPin.PIN_PERMISSION_CM
|
ClientPin.PIN_PERMISSION_CM else 0
|
||||||
else
|
}
|
||||||
0
|
|
||||||
|
private fun getPinPermissionsBE(fidoSession: YubiKitFidoSession): Int {
|
||||||
|
return if (BioEnrollment.isSupported(fidoSession.cachedInfo))
|
||||||
|
ClientPin.PIN_PERMISSION_BE else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unlockSession(
|
private fun unlockSession(
|
||||||
@ -250,13 +277,21 @@ class FidoManager(
|
|||||||
|
|
||||||
//fidoViewModel.setSessionLoadingState()
|
//fidoViewModel.setSessionLoadingState()
|
||||||
|
|
||||||
val permissions = getPermissions(fidoSession)
|
val pinPermissionsCM = getPinPermissionsCM(fidoSession)
|
||||||
|
val pinPermissionsBE = getPinPermissionsBE(fidoSession)
|
||||||
|
val permissions = pinPermissionsCM or pinPermissionsBE
|
||||||
|
|
||||||
if (permissions != 0) {
|
if (permissions != 0) {
|
||||||
val token = clientPin.getPinToken(pin, permissions, null)
|
val token = clientPin.getPinToken(pin, permissions, null)
|
||||||
val credentials = getCredentials(fidoSession, clientPin, token)
|
val credentials = getCredentials(fidoSession, clientPin, token)
|
||||||
logger.debug("Creds: {}", credentials)
|
logger.debug("Creds: {}", credentials)
|
||||||
fidoViewModel.updateCredentials(credentials)
|
fidoViewModel.updateCredentials(credentials)
|
||||||
|
|
||||||
|
if (pinPermissionsBE != 0) {
|
||||||
|
val fingerprints = getFingerprints(fidoSession, clientPin, token)
|
||||||
|
logger.debug("Fingerprints: {}", fingerprints)
|
||||||
|
fidoViewModel.updateFingerprints(fingerprints)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
|
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
|
||||||
}
|
}
|
||||||
@ -384,10 +419,8 @@ class FidoManager(
|
|||||||
val clientPin =
|
val clientPin =
|
||||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
|
|
||||||
val permissions = getPermissions(fidoSession)
|
val permissions = getPinPermissionsCM(fidoSession)
|
||||||
|
|
||||||
val token = clientPin.getPinToken(pinStore.getPin(), permissions, null)
|
val token = clientPin.getPinToken(pinStore.getPin(), permissions, null)
|
||||||
|
|
||||||
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
|
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
|
||||||
|
|
||||||
val credentialDescriptor =
|
val credentialDescriptor =
|
||||||
@ -413,6 +446,153 @@ class FidoManager(
|
|||||||
).toString()
|
).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFingerprints(
|
||||||
|
fidoSession: YubiKitFidoSession,
|
||||||
|
clientPin: ClientPin,
|
||||||
|
pinUvAuthToken: ByteArray
|
||||||
|
): List<FidoFingerprint> {
|
||||||
|
val bioEnrollment =
|
||||||
|
FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
|
||||||
|
|
||||||
|
val enrollments: Map<ByteArray, String?> = bioEnrollment.enumerateEnrollments()
|
||||||
|
return enrollments.map { enrollment ->
|
||||||
|
FidoFingerprint(HexCodec.bytesToHexString(enrollment.key), enrollment.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteFingerprint(templateId: String): String =
|
||||||
|
connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession ->
|
||||||
|
|
||||||
|
val clientPin =
|
||||||
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
|
|
||||||
|
val token =
|
||||||
|
clientPin.getPinToken(
|
||||||
|
pinStore.getPin(),
|
||||||
|
getPinPermissionsBE(fidoSession),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||||
|
bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId))
|
||||||
|
fidoViewModel.removeFingerprint(templateId)
|
||||||
|
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to true,
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun renameFingerprint(templateId: String, name: String): String =
|
||||||
|
connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession ->
|
||||||
|
|
||||||
|
val clientPin =
|
||||||
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
|
|
||||||
|
val token =
|
||||||
|
clientPin.getPinToken(
|
||||||
|
pinStore.getPin(),
|
||||||
|
getPinPermissionsBE(fidoSession),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||||
|
bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name)
|
||||||
|
fidoViewModel.renameFingerprint(templateId, name)
|
||||||
|
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to true,
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state : CommandState? = null
|
||||||
|
private fun cancelRegisterFingerprint(): String {
|
||||||
|
state?.cancel()
|
||||||
|
return NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun registerFingerprint(name: String?): String =
|
||||||
|
connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession ->
|
||||||
|
state?.cancel()
|
||||||
|
state = CommandState()
|
||||||
|
val clientPin =
|
||||||
|
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||||
|
|
||||||
|
val token =
|
||||||
|
clientPin.getPinToken(
|
||||||
|
pinStore.getPin(),
|
||||||
|
getPinPermissionsBE(fidoSession),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||||
|
|
||||||
|
val fingerprintEnrollmentContext = bioEnrollment.enroll(null)
|
||||||
|
var templateId: ByteArray? = null
|
||||||
|
while (templateId == null) {
|
||||||
|
try {
|
||||||
|
templateId = fingerprintEnrollmentContext.capture(state)
|
||||||
|
fidoViewModel.updateRegisterFpState(
|
||||||
|
createCaptureEvent(fingerprintEnrollmentContext.remaining!!)
|
||||||
|
)
|
||||||
|
} catch (captureError: FingerprintBioEnrollment.CaptureError) {
|
||||||
|
fidoViewModel.updateRegisterFpState(createCaptureErrorEvent(captureError.code))
|
||||||
|
} catch (ctapException: CtapException) {
|
||||||
|
when (ctapException.ctapError) {
|
||||||
|
CtapException.ERR_KEEPALIVE_CANCEL -> {
|
||||||
|
fingerprintEnrollmentContext.cancel()
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to false,
|
||||||
|
"status" to "user-cancelled"
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
CtapException.ERR_USER_ACTION_TIMEOUT -> {
|
||||||
|
fingerprintEnrollmentContext.cancel()
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to false,
|
||||||
|
"status" to "user-action-timeout"
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
else -> throw ctapException
|
||||||
|
}
|
||||||
|
} catch (io: IOException) {
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to false,
|
||||||
|
"status" to "connection-error"
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name.isNullOrBlank()) {
|
||||||
|
bioEnrollment.setName(templateId, name)
|
||||||
|
Logger.debug(logger, "Set name to {}", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
val templateIdHexString = HexCodec.bytesToHexString(templateId)
|
||||||
|
fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name))
|
||||||
|
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin()))
|
||||||
|
|
||||||
|
return@useSession JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"success" to true,
|
||||||
|
"template_id" to templateIdHexString,
|
||||||
|
"name" to name
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
if (!resetHelper.inProgress) {
|
if (!resetHelper.inProgress) {
|
||||||
fidoViewModel.clearSessionState()
|
fidoViewModel.clearSessionState()
|
||||||
|
@ -29,6 +29,8 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
@ -46,6 +48,24 @@ enum class FidoResetState(val value: String) {
|
|||||||
Touch("touch")
|
Touch("touch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class FidoRegisterFpEvent(val status: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FidoRegisterFpCaptureEvent(private val remaining: Int) : FidoRegisterFpEvent("capture")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FidoRegisterFpCaptureErrorEvent(val code: Int) : FidoRegisterFpEvent("capture-error")
|
||||||
|
|
||||||
|
|
||||||
|
fun createCaptureEvent(remaining: Int): FidoRegisterFpCaptureEvent {
|
||||||
|
return FidoRegisterFpCaptureEvent(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent {
|
||||||
|
return FidoRegisterFpCaptureErrorEvent(code)
|
||||||
|
}
|
||||||
|
|
||||||
class FidoResetHelper(
|
class FidoResetHelper(
|
||||||
private val deviceManager: DeviceManager,
|
private val deviceManager: DeviceManager,
|
||||||
private val fidoViewModel: FidoViewModel,
|
private val fidoViewModel: FidoViewModel,
|
||||||
|
@ -21,7 +21,9 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.yubico.authenticator.ViewModelData
|
import com.yubico.authenticator.ViewModelData
|
||||||
import com.yubico.authenticator.fido.data.FidoCredential
|
import com.yubico.authenticator.fido.data.FidoCredential
|
||||||
|
import com.yubico.authenticator.fido.data.FidoFingerprint
|
||||||
import com.yubico.authenticator.fido.data.Session
|
import com.yubico.authenticator.fido.data.Session
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
class FidoViewModel : ViewModel() {
|
class FidoViewModel : ViewModel() {
|
||||||
private val _sessionState = MutableLiveData<ViewModelData>()
|
private val _sessionState = MutableLiveData<ViewModelData>()
|
||||||
@ -60,4 +62,36 @@ class FidoViewModel : ViewModel() {
|
|||||||
fun updateResetState(resetState: FidoResetState) {
|
fun updateResetState(resetState: FidoResetState) {
|
||||||
_resetState.postValue(resetState.value)
|
_resetState.postValue(resetState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _fingerprints = MutableLiveData<List<FidoFingerprint>>()
|
||||||
|
val fingerprints: LiveData<List<FidoFingerprint>> = _fingerprints
|
||||||
|
|
||||||
|
fun updateFingerprints(fingerprints: List<FidoFingerprint>) {
|
||||||
|
_fingerprints.postValue(fingerprints)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFingerprint(fingerprint: FidoFingerprint) {
|
||||||
|
_fingerprints.postValue(_fingerprints.value?.plus(fingerprint))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFingerprint(templateId: String) {
|
||||||
|
_fingerprints.postValue(_fingerprints.value?.filter {
|
||||||
|
it.templateId != templateId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renameFingerprint(templateId: String, name: String) {
|
||||||
|
_fingerprints.postValue(_fingerprints.value?.map {
|
||||||
|
if (it.templateId == templateId) {
|
||||||
|
FidoFingerprint(templateId, name)
|
||||||
|
} else it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _registerFingerprint = MutableLiveData<FidoRegisterFpEvent>()
|
||||||
|
val registerFingerprint: LiveData<FidoRegisterFpEvent> = _registerFingerprint
|
||||||
|
|
||||||
|
fun updateRegisterFpState(registerFpState: FidoRegisterFpEvent) {
|
||||||
|
_registerFingerprint.postValue(registerFpState)
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.yubico.authenticator.fido.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FidoFingerprint(
|
||||||
|
@SerialName("template_id")
|
||||||
|
val templateId: String,
|
||||||
|
@SerialName("name")
|
||||||
|
val name: String?
|
||||||
|
)
|
@ -25,6 +25,7 @@ import '../../app/logging.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../desktop/models.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../exception/no_data_exception.dart';
|
import '../../exception/no_data_exception.dart';
|
||||||
import '../../exception/platform_exception_decoder.dart';
|
import '../../exception/platform_exception_decoder.dart';
|
||||||
@ -165,14 +166,91 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
|
|||||||
_FidoFingerprintsNotifier.new);
|
_FidoFingerprintsNotifier.new);
|
||||||
|
|
||||||
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||||
|
final _events = const EventChannel('android.fido.fingerprints');
|
||||||
|
late StreamSubscription _sub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
|
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
|
||||||
return [];
|
_sub = _events.receiveBroadcastStream().listen((event) {
|
||||||
|
final json = jsonDecode(event);
|
||||||
|
if (json == null) {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
} else {
|
||||||
|
List<Fingerprint> newState = List.from(
|
||||||
|
(json as List).map((e) => Fingerprint.fromJson(e)).toList());
|
||||||
|
state = AsyncValue.data(newState);
|
||||||
|
}
|
||||||
|
}, onError: (err, stackTrace) {
|
||||||
|
state = AsyncValue.error(err, stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(_sub.cancel);
|
||||||
|
return Completer<List<Fingerprint>>().future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<FingerprintEvent> registerFingerprint({String? name}) {
|
Stream<FingerprintEvent> registerFingerprint({String? name}) {
|
||||||
final controller = StreamController<FingerprintEvent>();
|
final controller = StreamController<FingerprintEvent>();
|
||||||
|
const registerEvents = EventChannel('android.fido.registerFp');
|
||||||
|
|
||||||
|
final registerFpSub =
|
||||||
|
registerEvents.receiveBroadcastStream().skip(1).listen((event) {
|
||||||
|
if (controller.isClosed) {
|
||||||
|
_log.debug('Controller already closed, ignoring: $event');
|
||||||
|
}
|
||||||
|
_log.debug('Received register fingerprint event: $event');
|
||||||
|
if (event is String && event.isNotEmpty) {
|
||||||
|
final e = jsonDecode(event);
|
||||||
|
_log.debug('Received register fingerprint event: $e');
|
||||||
|
|
||||||
|
final status = e['status'];
|
||||||
|
|
||||||
|
controller.sink.add(switch (status) {
|
||||||
|
'capture' => FingerprintEvent.capture(e['remaining']),
|
||||||
|
'capture-error' => FingerprintEvent.error(e['code']),
|
||||||
|
final other => throw UnimplementedError(other)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.onCancel = () async {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
_log.debug('Cancelling fingerprint registration');
|
||||||
|
await _methods.invokeMethod('cancelRegisterFingerprint');
|
||||||
|
await registerFpSub.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.onListen = () async {
|
||||||
|
try {
|
||||||
|
final registerFpResult =
|
||||||
|
await _methods.invokeMethod('registerFingerprint', {'name': name});
|
||||||
|
|
||||||
|
_log.debug('Finished registerFingerprint with: $registerFpResult');
|
||||||
|
|
||||||
|
final resultJson = jsonDecode(registerFpResult);
|
||||||
|
|
||||||
|
if (resultJson['success'] == true) {
|
||||||
|
controller.sink
|
||||||
|
.add(FingerprintEvent.complete(Fingerprint.fromJson(resultJson)));
|
||||||
|
} else {
|
||||||
|
// TODO abstract platform errors
|
||||||
|
final errorStatus = resultJson['status'];
|
||||||
|
if (errorStatus != 'user-cancelled') {
|
||||||
|
throw RpcError(errorStatus, 'Platform error: $errorStatus', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on PlatformException catch (pe) {
|
||||||
|
_log.debug('Received platform exception: \'$pe\'');
|
||||||
|
final decoded = pe.decode();
|
||||||
|
controller.sink.addError(decoded);
|
||||||
|
} catch (e) {
|
||||||
|
_log.debug('Received error: \'$e\'');
|
||||||
|
controller.sink.addError(e);
|
||||||
|
} finally {
|
||||||
|
await controller.sink.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return controller.stream;
|
return controller.stream;
|
||||||
}
|
}
|
||||||
@ -180,11 +258,60 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|||||||
@override
|
@override
|
||||||
Future<Fingerprint> renameFingerprint(
|
Future<Fingerprint> renameFingerprint(
|
||||||
Fingerprint fingerprint, String name) async {
|
Fingerprint fingerprint, String name) async {
|
||||||
return fingerprint;
|
try {
|
||||||
|
final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod(
|
||||||
|
'renameFingerprint',
|
||||||
|
{
|
||||||
|
'templateId': fingerprint.templateId,
|
||||||
|
'name': name,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
if (renameFingerprintResponse['success'] == true) {
|
||||||
|
_log.debug('FIDO rename fingerprint succeeded');
|
||||||
|
return Fingerprint(fingerprint.templateId, name);
|
||||||
|
} else {
|
||||||
|
_log.debug('FIDO rename fingerprint failed');
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
} on PlatformException catch (pe) {
|
||||||
|
var decodedException = pe.decode();
|
||||||
|
if (decodedException is CancellationException) {
|
||||||
|
_log.debug('User cancelled rename fingerprint FIDO operation');
|
||||||
|
} else {
|
||||||
|
_log.error('Rename fingerprint FIDO operation failed.', pe);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw decodedException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteFingerprint(Fingerprint fingerprint) async {}
|
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
||||||
|
try {
|
||||||
|
final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod(
|
||||||
|
'deleteFingerprint',
|
||||||
|
{
|
||||||
|
'templateId': fingerprint.templateId,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
if (deleteFingerprintResponse['success'] == true) {
|
||||||
|
_log.debug('FIDO delete fingerprint succeeded');
|
||||||
|
} else {
|
||||||
|
_log.debug('FIDO delete fingerprint failed');
|
||||||
|
}
|
||||||
|
} on PlatformException catch (pe) {
|
||||||
|
var decodedException = pe.decode();
|
||||||
|
if (decodedException is CancellationException) {
|
||||||
|
_log.debug('User cancelled delete fingerprint FIDO operation');
|
||||||
|
} else {
|
||||||
|
_log.error('Delete fingerprint FIDO operation failed.', pe);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw decodedException;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final androidCredentialProvider = AsyncNotifierProvider.autoDispose
|
final androidCredentialProvider = AsyncNotifierProvider.autoDispose
|
||||||
|
@ -85,12 +85,20 @@ Future<Widget> initialize() async {
|
|||||||
),
|
),
|
||||||
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
|
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
|
||||||
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
||||||
supportedSectionsProvider.overrideWithValue(
|
supportedSectionsProvider.overrideWithValue([
|
||||||
[Section.home, Section.accounts, Section.passkeys]),
|
Section.home,
|
||||||
|
Section.accounts,
|
||||||
|
Section.fingerprints,
|
||||||
|
Section.passkeys
|
||||||
|
]),
|
||||||
// this specifies the priority of sections to show when
|
// this specifies the priority of sections to show when
|
||||||
// the connected YubiKey does not support current section
|
// the connected YubiKey does not support current section
|
||||||
androidSectionPriority.overrideWithValue(
|
androidSectionPriority.overrideWithValue([
|
||||||
[Section.accounts, Section.passkeys, Section.home]),
|
Section.accounts,
|
||||||
|
Section.fingerprints,
|
||||||
|
Section.passkeys,
|
||||||
|
Section.home
|
||||||
|
]),
|
||||||
supportedThemesProvider.overrideWith(
|
supportedThemesProvider.overrideWith(
|
||||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||||
),
|
),
|
||||||
|
@ -78,6 +78,8 @@ enum _DDesc {
|
|||||||
fidoUnlockSession,
|
fidoUnlockSession,
|
||||||
fidoSetPin,
|
fidoSetPin,
|
||||||
fidoDeleteCredential,
|
fidoDeleteCredential,
|
||||||
|
fidoDeleteFingerprint,
|
||||||
|
fidoRenameFingerprint,
|
||||||
fidoActionFailure,
|
fidoActionFailure,
|
||||||
// Others
|
// Others
|
||||||
invalid;
|
invalid;
|
||||||
@ -101,7 +103,9 @@ enum _DDesc {
|
|||||||
dialogDescriptionFidoIndex + 1: fidoUnlockSession,
|
dialogDescriptionFidoIndex + 1: fidoUnlockSession,
|
||||||
dialogDescriptionFidoIndex + 2: fidoSetPin,
|
dialogDescriptionFidoIndex + 2: fidoSetPin,
|
||||||
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
|
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
|
||||||
dialogDescriptionFidoIndex + 4: fidoActionFailure,
|
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
|
||||||
|
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
|
||||||
|
dialogDescriptionFidoIndex + 6: fidoActionFailure,
|
||||||
}[id] ??
|
}[id] ??
|
||||||
_DDesc.invalid;
|
_DDesc.invalid;
|
||||||
}
|
}
|
||||||
@ -179,6 +183,8 @@ class _DialogProvider {
|
|||||||
_DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock,
|
_DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock,
|
||||||
_DDesc.fidoSetPin => l10n.l_nfc_dialog_fido_set_pin,
|
_DDesc.fidoSetPin => l10n.l_nfc_dialog_fido_set_pin,
|
||||||
_DDesc.fidoDeleteCredential => l10n.s_nfc_dialog_fido_delete_credential,
|
_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,
|
_DDesc.fidoActionFailure => l10n.s_nfc_dialog_fido_failure,
|
||||||
_ => ''
|
_ => ''
|
||||||
};
|
};
|
||||||
|
@ -63,6 +63,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_animator.dispose();
|
_animator.dispose();
|
||||||
_nameFocus.dispose();
|
_nameFocus.dispose();
|
||||||
|
_subscription.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -838,6 +838,8 @@
|
|||||||
"s_nfc_dialog_fido_unlock": null,
|
"s_nfc_dialog_fido_unlock": null,
|
||||||
"l_nfc_dialog_fido_set_pin": null,
|
"l_nfc_dialog_fido_set_pin": null,
|
||||||
"s_nfc_dialog_fido_delete_credential": null,
|
"s_nfc_dialog_fido_delete_credential": null,
|
||||||
|
"s_nfc_dialog_fido_delete_fingerprint": null,
|
||||||
|
"s_nfc_dialog_fido_rename_fingerprint": null,
|
||||||
"s_nfc_dialog_fido_failure": null,
|
"s_nfc_dialog_fido_failure": null,
|
||||||
|
|
||||||
"@_ndef": {},
|
"@_ndef": {},
|
||||||
|
@ -838,6 +838,8 @@
|
|||||||
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO applet",
|
"s_nfc_dialog_fido_unlock": "Action: unlock FIDO applet",
|
||||||
"l_nfc_dialog_fido_set_pin": "Action: set or change FIDO applet PIN",
|
"l_nfc_dialog_fido_set_pin": "Action: set or change FIDO applet PIN",
|
||||||
"s_nfc_dialog_fido_delete_credential": "Action: delete Passkey",
|
"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",
|
"s_nfc_dialog_fido_failure": "FIDO operation failed",
|
||||||
|
|
||||||
"@_ndef": {},
|
"@_ndef": {},
|
||||||
|
@ -838,6 +838,8 @@
|
|||||||
"s_nfc_dialog_fido_unlock": null,
|
"s_nfc_dialog_fido_unlock": null,
|
||||||
"l_nfc_dialog_fido_set_pin": null,
|
"l_nfc_dialog_fido_set_pin": null,
|
||||||
"s_nfc_dialog_fido_delete_credential": null,
|
"s_nfc_dialog_fido_delete_credential": null,
|
||||||
|
"s_nfc_dialog_fido_delete_fingerprint": null,
|
||||||
|
"s_nfc_dialog_fido_rename_fingerprint": null,
|
||||||
"s_nfc_dialog_fido_failure": null,
|
"s_nfc_dialog_fido_failure": null,
|
||||||
|
|
||||||
"@_ndef": {},
|
"@_ndef": {},
|
||||||
|
@ -838,6 +838,8 @@
|
|||||||
"s_nfc_dialog_fido_unlock": null,
|
"s_nfc_dialog_fido_unlock": null,
|
||||||
"l_nfc_dialog_fido_set_pin": null,
|
"l_nfc_dialog_fido_set_pin": null,
|
||||||
"s_nfc_dialog_fido_delete_credential": null,
|
"s_nfc_dialog_fido_delete_credential": null,
|
||||||
|
"s_nfc_dialog_fido_delete_fingerprint": null,
|
||||||
|
"s_nfc_dialog_fido_rename_fingerprint": null,
|
||||||
"s_nfc_dialog_fido_failure": null,
|
"s_nfc_dialog_fido_failure": null,
|
||||||
|
|
||||||
"@_ndef": {},
|
"@_ndef": {},
|
||||||
|
@ -838,6 +838,8 @@
|
|||||||
"s_nfc_dialog_fido_unlock": null,
|
"s_nfc_dialog_fido_unlock": null,
|
||||||
"l_nfc_dialog_fido_set_pin": null,
|
"l_nfc_dialog_fido_set_pin": null,
|
||||||
"s_nfc_dialog_fido_delete_credential": null,
|
"s_nfc_dialog_fido_delete_credential": null,
|
||||||
|
"s_nfc_dialog_fido_delete_fingerprint": null,
|
||||||
|
"s_nfc_dialog_fido_rename_fingerprint": null,
|
||||||
"s_nfc_dialog_fido_failure": null,
|
"s_nfc_dialog_fido_failure": null,
|
||||||
|
|
||||||
"@_ndef": {},
|
"@_ndef": {},
|
||||||
|
Loading…
Reference in New Issue
Block a user