This commit is contained in:
Adam Velebil 2024-03-12 18:00:06 +01:00
parent a9a5532067
commit ff8f2c8ce0
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
24 changed files with 244 additions and 179 deletions

View File

@ -32,6 +32,22 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
interface JsonSerializable {
fun toJson() : String
}
sealed interface SessionState {
data object Empty : SessionState
data object Loading : SessionState
data class Value<T : JsonSerializable>(val data: T) : SessionState
}
data class ChannelData<T>(val data: T?, val isLoading: Boolean = false) {
companion object {
fun <T> loading() = ChannelData<T>(null, true)
fun <T> empty() = ChannelData<T>(null)
}
}
/** /**
* Observes a LiveData value, sending each change to Flutter via an EventChannel. * Observes a LiveData value, sending each change to Flutter via an EventChannel.
*/ */
@ -61,6 +77,83 @@ inline fun <reified T> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, mess
} }
} }
/**
* Observes a Loadable LiveData value, sending each change to Flutter via an EventChannel.
*/
inline fun <reified T> LiveData<ChannelData<T>>.streamData(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 {
if (it.isLoading) LOADING
else it.data?.let(jsonSerializer::encodeToString) ?: NULL
} ?: NULL
)
}
override fun onCancel(arguments: Any?) {
sink = null
}
})
val observer = Observer<ChannelData<T>> {
sink?.success(
if (it.isLoading) LOADING
else it.data?.let(jsonSerializer::encodeToString) ?: NULL
)
}
observe(lifecycleOwner, observer)
return Closeable {
removeObserver(observer)
channel.setStreamHandler(null)
}
}
fun get(state: SessionState) : String = when (state) {
is SessionState.Empty -> NULL
is SessionState.Loading -> LOADING
is SessionState.Value<*> -> state.data.toJson()
}
/**
* Observes a Loadable LiveData value, sending each change to Flutter via an EventChannel.
*/
inline fun <reified T : SessionState> LiveData<T>.streamState(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 {
get(it)
} ?: NULL
)
}
override fun onCancel(arguments: Any?) {
sink = null
}
})
val observer = Observer<T> {
sink?.success(get(it))
}
observe(lifecycleOwner, observer)
return Closeable {
removeObserver(observer)
channel.setStreamHandler(null)
}
}
typealias MethodHandler = suspend (method: String, args: Map<String, Any?>) -> String typealias MethodHandler = suspend (method: String, args: Map<String, Any?>) -> String
/** /**

View File

@ -20,6 +20,8 @@ import kotlinx.serialization.json.Json
const val NULL = "null" const val NULL = "null"
const val LOADING = """{ "loading": true }"""
val jsonSerializer = Json { val jsonSerializer = Json {
// creates properties for default values // creates properties for default values
encodeDefaults = true encodeDefaults = true

View File

@ -320,10 +320,10 @@ class MainActivity : FlutterFragmentActivity() {
flutterStreams = listOf( flutterStreams = listOf(
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"), viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"), oathViewModel.sessionState.streamState(this, messenger, "android.oath.sessionState"),
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.streamData(this, messenger, "android.fido.sessionState"),
fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"), fidoViewModel.credentials.streamData(this, messenger, "android.fido.credentials"),
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"), fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
) )

View File

@ -140,7 +140,7 @@ class FidoManager(
if (!deviceManager.isUsbKeyConnected()) { if (!deviceManager.isUsbKeyConnected()) {
// for NFC connections require extra tap when switching context // for NFC connections require extra tap when switching context
if (fidoViewModel.sessionState.value == null) { if (fidoViewModel.sessionState.value == null) {
fidoViewModel.setSessionState(Session.uninitialized) fidoViewModel.clearSessionState()
} }
} }
@ -150,8 +150,8 @@ class FidoManager(
super.dispose() super.dispose()
deviceManager.removeDeviceListener(this) deviceManager.removeDeviceListener(this)
fidoChannel.setMethodCallHandler(null) fidoChannel.setMethodCallHandler(null)
fidoViewModel.setSessionState(null) fidoViewModel.clearSessionState()
fidoViewModel.updateCredentials(listOf()) fidoViewModel.clearCredentials()
coroutineScope.cancel() coroutineScope.cancel()
} }
@ -185,7 +185,7 @@ class FidoManager(
} }
// Clear any cached FIDO state // Clear any cached FIDO state
fidoViewModel.setSessionState(null) fidoViewModel.clearSessionState()
} }
} }
@ -198,7 +198,7 @@ class FidoManager(
YubiKitFidoSession(connection as SmartCardConnection) YubiKitFidoSession(connection as SmartCardConnection)
} }
val previousSession = fidoViewModel.sessionState.value?.info val previousSession = fidoViewModel.sessionState.value?.data?.info
val currentSession = SessionInfo(fidoSession.cachedInfo) val currentSession = SessionInfo(fidoSession.cachedInfo)
logger.debug( logger.debug(
"Previous session: {}, current session: {}", "Previous session: {}, current session: {}",
@ -253,6 +253,8 @@ class FidoManager(
pin: CharArray pin: CharArray
): String { ): String {
fidoViewModel.setSessionLoadingState()
val permissions = getPermissions(fidoSession) val permissions = getPermissions(fidoSession)
if (permissions != 0) { if (permissions != 0) {
@ -260,7 +262,6 @@ class FidoManager(
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)
} else { } else {
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com") clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
} }
@ -285,7 +286,7 @@ class FidoManager(
ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED
) { ) {
pinStore.setPin(null) pinStore.setPin(null)
fidoViewModel.updateCredentials(emptyList()) fidoViewModel.clearCredentials()
val pinRetriesResult = clientPin.pinRetries val pinRetriesResult = clientPin.pinRetries
JSONObject( JSONObject(
mapOf( mapOf(
@ -355,6 +356,9 @@ class FidoManager(
pinUvAuthToken: ByteArray pinUvAuthToken: ByteArray
): List<FidoCredential> = ): List<FidoCredential> =
try { try {
fidoViewModel.setCredentialsLoadingState()
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, pinUvAuthToken) val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
val rpIds = credMan.enumerateRps() val rpIds = credMan.enumerateRps()
@ -390,7 +394,7 @@ class FidoManager(
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token) val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
val credentialDescriptor = val credentialDescriptor =
fidoViewModel.credentials.value?.firstOrNull { fidoViewModel.credentials.value?.data?.firstOrNull {
it.credentialId == credentialId && it.rpId == rpId it.credentialId == credentialId && it.rpId == rpId
}?.publicKeyCredentialDescriptor }?.publicKeyCredentialDescriptor
@ -414,11 +418,11 @@ class FidoManager(
override fun onDisconnected() { override fun onDisconnected() {
if (!resetHelper.inProgress) { if (!resetHelper.inProgress) {
fidoViewModel.setSessionState(null) fidoViewModel.clearSessionState()
} }
} }
override fun onTimeout() { override fun onTimeout() {
fidoViewModel.setSessionState(null) fidoViewModel.clearSessionState()
} }
} }

View File

@ -202,7 +202,7 @@ class FidoResetHelper(
logger.debug("Calling FIDO reset") logger.debug("Calling FIDO reset")
fidoSession.reset(resetCommandState) fidoSession.reset(resetCommandState)
fidoViewModel.setSessionState(Session(fidoSession.info, true)) fidoViewModel.setSessionState(Session(fidoSession.info, true))
fidoViewModel.updateCredentials(emptyList()) fidoViewModel.clearCredentials()
pinStore.setPin(null) pinStore.setPin(null)
} }

View File

@ -19,28 +19,45 @@ package com.yubico.authenticator.fido
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.yubico.authenticator.ChannelData
import com.yubico.authenticator.fido.data.FidoCredential import com.yubico.authenticator.fido.data.FidoCredential
import com.yubico.authenticator.fido.data.Session import com.yubico.authenticator.fido.data.Session
class FidoViewModel : ViewModel() { class FidoViewModel : ViewModel() {
private val _sessionState = MutableLiveData<Session?>(null) private val _sessionState = MutableLiveData<ChannelData<Session?>>()
val sessionState: LiveData<Session?> = _sessionState val sessionState: LiveData<ChannelData<Session?>> = _sessionState
fun setSessionState(sessionState: Session?) { fun setSessionState(sessionState: Session?) {
_sessionState.postValue(sessionState) _sessionState.postValue(ChannelData(sessionState))
} }
private val _credentials = MutableLiveData<List<FidoCredential>>() fun clearSessionState() {
val credentials: LiveData<List<FidoCredential>> = _credentials _sessionState.postValue(ChannelData.empty())
}
fun setSessionLoadingState() {
_sessionState.postValue(ChannelData.loading())
}
private val _credentials = MutableLiveData<ChannelData<List<FidoCredential>?>>()
val credentials: LiveData<ChannelData<List<FidoCredential>?>> = _credentials
fun setCredentialsLoadingState() {
_credentials.postValue(ChannelData.loading())
}
fun updateCredentials(credentials: List<FidoCredential>) { fun updateCredentials(credentials: List<FidoCredential>) {
_credentials.postValue(credentials) _credentials.postValue(ChannelData(credentials))
}
fun clearCredentials() {
_credentials.postValue(ChannelData.empty())
} }
fun removeCredential(rpId: String, credentialId: String) { fun removeCredential(rpId: String, credentialId: String) {
_credentials.postValue(_credentials.value?.filter { _credentials.postValue(ChannelData(_credentials.value?.data?.filter {
it.credentialId != credentialId || it.rpId != rpId it.credentialId != credentialId || it.rpId != rpId
}) }))
} }
private val _resetState = MutableLiveData(FidoResetState.Remove.value) private val _resetState = MutableLiveData(FidoResetState.Remove.value)

View File

@ -86,30 +86,9 @@ data class SessionInfo(
data class Session( data class Session(
@SerialName("info") @SerialName("info")
val info: SessionInfo, val info: SessionInfo,
val unlocked: Boolean, val unlocked: Boolean
val initialized: Boolean
) { ) {
constructor(infoData: InfoData, unlocked: Boolean) : this( constructor(infoData: InfoData, unlocked: Boolean) : this(
SessionInfo(infoData), unlocked, true SessionInfo(infoData), unlocked
)
companion object {
val uninitialized = Session(
SessionInfo(
Options(
clientPin = false,
credMgmt = false,
credentialMgmtPreview = false,
bioEnroll = null,
alwaysUv = false
),
aaguid = ByteArray(0),
minPinLength = 0,
forcePinChange = false
),
unlocked = false,
initialized = false
) )
} }
}

View File

@ -203,8 +203,8 @@ class OathManager(
if (!deviceManager.isUsbKeyConnected()) { if (!deviceManager.isUsbKeyConnected()) {
// for NFC connections require extra tap when switching context // for NFC connections require extra tap when switching context
if (oathViewModel.sessionState.value == null) { if (oathViewModel.sessionState.value is SessionState.Empty) {
oathViewModel.setSessionState(Session.uninitialized) oathViewModel.setSessionState(null)
} }
} }
} }
@ -223,7 +223,7 @@ class OathManager(
try { try {
device.withConnection<SmartCardConnection, Unit> { connection -> device.withConnection<SmartCardConnection, Unit> { connection ->
val session = getOathSession(connection) val session = getOathSession(connection)
val previousId = oathViewModel.sessionState.value?.deviceId val previousId = oathViewModel.currentSession()?.deviceId
if (session.deviceId == previousId && device is NfcYubiKeyDevice) { if (session.deviceId == previousId && device is NfcYubiKeyDevice) {
// Run any pending action // Run any pending action
pendingAction?.let { action -> pendingAction?.let { action ->
@ -468,7 +468,7 @@ class OathManager(
private fun forgetPassword(): String { private fun forgetPassword(): String {
keyManager.clearAll() keyManager.clearAll()
logger.debug("Cleared all keys.") logger.debug("Cleared all keys.")
oathViewModel.sessionState.value?.let { oathViewModel.currentSession()?.let {
oathViewModel.setSessionState( oathViewModel.setSessionState(
it.copy( it.copy(
isLocked = it.isAccessKeySet, isLocked = it.isAccessKeySet,

View File

@ -19,25 +19,36 @@ package com.yubico.authenticator.oath
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.yubico.authenticator.SessionState
import com.yubico.authenticator.JsonSerializable
import com.yubico.authenticator.oath.data.Code import com.yubico.authenticator.oath.data.Code
import com.yubico.authenticator.oath.data.Credential import com.yubico.authenticator.oath.data.Credential
import com.yubico.authenticator.oath.data.CredentialWithCode import com.yubico.authenticator.oath.data.CredentialWithCode
import com.yubico.authenticator.oath.data.Session import com.yubico.authenticator.oath.data.Session
class OathViewModel: ViewModel() { class OathViewModel: ViewModel() {
private val _sessionState = MutableLiveData<Session?>()
val sessionState: LiveData<Session?> = _sessionState private val _sessionState = MutableLiveData<SessionState>()
val sessionState: LiveData<SessionState> = _sessionState
fun currentSession() : Session? = (sessionState.value as? SessionState.Value<*>)?.data as Session?
// Sets session and credentials after performing OATH reset // Sets session and credentials after performing OATH reset
// Note: we cannot use [setSessionState] because resetting OATH changes deviceId // Note: we cannot use [setSessionState] because resetting OATH changes deviceId
fun resetOathSession(sessionState: Session, credentials: Map<Credential, Code?>) { fun resetOathSession(sessionState: Session, credentials: Map<Credential, Code?>) {
_sessionState.postValue(sessionState) _sessionState.postValue(SessionState.Value(sessionState))
updateCredentials(credentials) updateCredentials(credentials)
} }
fun setSessionState(sessionState: Session?) { fun setSessionState(sessionState: Session?) {
val oldDeviceId = _sessionState.value?.deviceId val oldDeviceId = currentSession()?.deviceId
_sessionState.postValue(sessionState)
if (sessionState == null) {
_sessionState.postValue(SessionState.Empty)
} else {
_sessionState.postValue(SessionState.Value(sessionState))
}
if(oldDeviceId != sessionState?.deviceId) { if(oldDeviceId != sessionState?.deviceId) {
_credentials.postValue(null) _credentials.postValue(null)
} }
@ -59,7 +70,7 @@ class OathViewModel: ViewModel() {
} }
fun addCredential(credential: Credential, code: Code?): CredentialWithCode { fun addCredential(credential: Credential, code: Code?): CredentialWithCode {
require(credential.deviceId == _sessionState.value?.deviceId) { require(credential.deviceId == currentSession()?.deviceId) {
"Cannot add credential for different deviceId" "Cannot add credential for different deviceId"
} }
return CredentialWithCode(credential, code).also { return CredentialWithCode(credential, code).also {

View File

@ -16,10 +16,13 @@
package com.yubico.authenticator.oath.data package com.yubico.authenticator.oath.data
import com.yubico.authenticator.JsonSerializable
import com.yubico.authenticator.device.Version import com.yubico.authenticator.device.Version
import com.yubico.authenticator.jsonSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
typealias YubiKitOathSession = com.yubico.yubikit.oath.OathSession typealias YubiKitOathSession = com.yubico.yubikit.oath.OathSession
@ -34,9 +37,8 @@ data class Session(
@SerialName("remembered") @SerialName("remembered")
val isRemembered: Boolean, val isRemembered: Boolean,
@SerialName("locked") @SerialName("locked")
val isLocked: Boolean, val isLocked: Boolean
val initialized: Boolean ) : JsonSerializable {
) {
@SerialName("keystore") @SerialName("keystore")
@Suppress("unused") @Suppress("unused")
val keystoreState: String = "unknown" val keystoreState: String = "unknown"
@ -51,18 +53,10 @@ data class Session(
), ),
oathSession.isAccessKeySet, oathSession.isAccessKeySet,
isRemembered, isRemembered,
oathSession.isLocked, oathSession.isLocked
initialized = true
) )
companion object { override fun toJson(): String {
val uninitialized = Session( return jsonSerializer.encodeToString(this)
deviceId = "",
version = Version(0, 0, 0),
isAccessKeySet = false,
isRemembered = false,
isLocked = false,
initialized = false
)
} }
} }

View File

@ -33,17 +33,19 @@ final _log = Logger('android.fido.state');
const _methods = MethodChannel('android.fido.methods'); const _methods = MethodChannel('android.fido.methods');
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new); .family<FidoStateNotifier, FidoState?, DevicePath>(_FidoStateNotifier.new);
class _FidoStateNotifier extends FidoStateNotifier { class _FidoStateNotifier extends FidoStateNotifier {
final _events = const EventChannel('android.fido.sessionState'); final _events = const EventChannel('android.fido.sessionState');
late StreamSubscription _sub; late StreamSubscription _sub;
@override @override
FutureOr<FidoState> build(DevicePath devicePath) async { FutureOr<FidoState?> build(DevicePath devicePath) async {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = const AsyncValue.data(null);
} else if (json.containsKey('loading') && json['loading'] == true) {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
final fidoState = FidoState.fromJson(json); final fidoState = FidoState.fromJson(json);
@ -55,7 +57,7 @@ class _FidoStateNotifier extends FidoStateNotifier {
ref.onDispose(_sub.cancel); ref.onDispose(_sub.cancel);
return Completer<FidoState>().future; return Completer<FidoState?>().future;
} }
@override @override
@ -177,7 +179,7 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
} }
final androidCredentialProvider = AsyncNotifierProvider.autoDispose final androidCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>( .family<FidoCredentialsNotifier, List<FidoCredential>?, DevicePath>(
_FidoCredentialsNotifier.new); _FidoCredentialsNotifier.new);
class _FidoCredentialsNotifier extends FidoCredentialsNotifier { class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
@ -185,10 +187,12 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
late StreamSubscription _sub; late StreamSubscription _sub;
@override @override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async { FutureOr<List<FidoCredential>?> build(DevicePath devicePath) async {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = const AsyncValue.data(null);
} else if (json[0] is Map && json[0]['loading'] == true) {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
List<FidoCredential> newState = List.from( List<FidoCredential> newState = List.from(
@ -200,7 +204,7 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
}); });
ref.onDispose(_sub.cancel); ref.onDispose(_sub.cancel);
return Completer<List<FidoCredential>>().future; return Completer<List<FidoCredential>?>().future;
} }
@override @override

View File

@ -24,6 +24,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/logging.dart'; import '../../app/logging.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
@ -51,6 +52,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
_sub = _events.receiveBroadcastStream().listen((event) { _sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
if (json == null) { if (json == null) {
state = AsyncValue.error(const ErrorDataEmpty(), StackTrace.current);
} else if (json[0] is Map && json[0]['loading'] == true) {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
} else { } else {
final oathState = OathState.fromJson(json); final oathState = OathState.fromJson(json);

View File

@ -0,0 +1,3 @@
class ErrorDataEmpty {
const ErrorDataEmpty();
}

View File

@ -47,14 +47,14 @@ final _sessionProvider =
); );
final desktopFidoState = AsyncNotifierProvider.autoDispose final desktopFidoState = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>( .family<FidoStateNotifier, FidoState?, DevicePath>(
_DesktopFidoStateNotifier.new); _DesktopFidoStateNotifier.new);
class _DesktopFidoStateNotifier extends FidoStateNotifier { class _DesktopFidoStateNotifier extends FidoStateNotifier {
late RpcNodeSession _session; late RpcNodeSession _session;
late StateController<String?> _pinController; late StateController<String?> _pinController;
FutureOr<FidoState> _build(DevicePath devicePath) async { FutureOr<FidoState?> _build(DevicePath devicePath) async {
var result = await _session.command('get'); var result = await _session.command('get');
FidoState fidoState = FidoState.fromJson(result['data']); FidoState fidoState = FidoState.fromJson(result['data']);
if (fidoState.hasPin && !fidoState.unlocked) { if (fidoState.hasPin && !fidoState.unlocked) {
@ -71,7 +71,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
} }
@override @override
FutureOr<FidoState> build(DevicePath devicePath) async { FutureOr<FidoState?> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath)); _session = ref.watch(_sessionProvider(devicePath));
if (Platform.isWindows) { if (Platform.isWindows) {
// Make sure to rebuild if isAdmin changes // Make sure to rebuild if isAdmin changes
@ -269,7 +269,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
} }
final desktopCredentialProvider = AsyncNotifierProvider.autoDispose final desktopCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>( .family<FidoCredentialsNotifier, List<FidoCredential>?, DevicePath>(
_DesktopFidoCredentialsNotifier.new); _DesktopFidoCredentialsNotifier.new);
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {

View File

@ -27,8 +27,7 @@ class FidoState with _$FidoState {
factory FidoState( factory FidoState(
{required Map<String, dynamic> info, {required Map<String, dynamic> info,
required bool unlocked, required bool unlocked}) = _FidoState;
@Default(true) bool initialized}) = _FidoState;
factory FidoState.fromJson(Map<String, dynamic> json) => factory FidoState.fromJson(Map<String, dynamic> json) =>
_$FidoStateFromJson(json); _$FidoStateFromJson(json);

View File

@ -22,7 +22,6 @@ FidoState _$FidoStateFromJson(Map<String, dynamic> json) {
mixin _$FidoState { mixin _$FidoState {
Map<String, dynamic> get info => throw _privateConstructorUsedError; Map<String, dynamic> get info => throw _privateConstructorUsedError;
bool get unlocked => throw _privateConstructorUsedError; bool get unlocked => throw _privateConstructorUsedError;
bool get initialized => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -35,7 +34,7 @@ abstract class $FidoStateCopyWith<$Res> {
factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) = factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) =
_$FidoStateCopyWithImpl<$Res, FidoState>; _$FidoStateCopyWithImpl<$Res, FidoState>;
@useResult @useResult
$Res call({Map<String, dynamic> info, bool unlocked, bool initialized}); $Res call({Map<String, dynamic> info, bool unlocked});
} }
/// @nodoc /// @nodoc
@ -53,7 +52,6 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
$Res call({ $Res call({
Object? info = null, Object? info = null,
Object? unlocked = null, Object? unlocked = null,
Object? initialized = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
info: null == info info: null == info
@ -64,10 +62,6 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
? _value.unlocked ? _value.unlocked
: unlocked // ignore: cast_nullable_to_non_nullable : unlocked // ignore: cast_nullable_to_non_nullable
as bool, as bool,
initialized: null == initialized
? _value.initialized
: initialized // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
} }
@ -80,7 +74,7 @@ abstract class _$$FidoStateImplCopyWith<$Res>
__$$FidoStateImplCopyWithImpl<$Res>; __$$FidoStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({Map<String, dynamic> info, bool unlocked, bool initialized}); $Res call({Map<String, dynamic> info, bool unlocked});
} }
/// @nodoc /// @nodoc
@ -96,7 +90,6 @@ class __$$FidoStateImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? info = null, Object? info = null,
Object? unlocked = null, Object? unlocked = null,
Object? initialized = null,
}) { }) {
return _then(_$FidoStateImpl( return _then(_$FidoStateImpl(
info: null == info info: null == info
@ -107,10 +100,6 @@ class __$$FidoStateImplCopyWithImpl<$Res>
? _value.unlocked ? _value.unlocked
: unlocked // ignore: cast_nullable_to_non_nullable : unlocked // ignore: cast_nullable_to_non_nullable
as bool, as bool,
initialized: null == initialized
? _value.initialized
: initialized // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@ -119,9 +108,7 @@ class __$$FidoStateImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$FidoStateImpl extends _FidoState { class _$FidoStateImpl extends _FidoState {
_$FidoStateImpl( _$FidoStateImpl(
{required final Map<String, dynamic> info, {required final Map<String, dynamic> info, required this.unlocked})
required this.unlocked,
this.initialized = true})
: _info = info, : _info = info,
super._(); super._();
@ -138,13 +125,10 @@ class _$FidoStateImpl extends _FidoState {
@override @override
final bool unlocked; final bool unlocked;
@override
@JsonKey()
final bool initialized;
@override @override
String toString() { String toString() {
return 'FidoState(info: $info, unlocked: $unlocked, initialized: $initialized)'; return 'FidoState(info: $info, unlocked: $unlocked)';
} }
@override @override
@ -154,15 +138,13 @@ class _$FidoStateImpl extends _FidoState {
other is _$FidoStateImpl && other is _$FidoStateImpl &&
const DeepCollectionEquality().equals(other._info, _info) && const DeepCollectionEquality().equals(other._info, _info) &&
(identical(other.unlocked, unlocked) || (identical(other.unlocked, unlocked) ||
other.unlocked == unlocked) && other.unlocked == unlocked));
(identical(other.initialized, initialized) ||
other.initialized == initialized));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash(runtimeType, int get hashCode => Object.hash(
const DeepCollectionEquality().hash(_info), unlocked, initialized); runtimeType, const DeepCollectionEquality().hash(_info), unlocked);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -181,8 +163,7 @@ class _$FidoStateImpl extends _FidoState {
abstract class _FidoState extends FidoState { abstract class _FidoState extends FidoState {
factory _FidoState( factory _FidoState(
{required final Map<String, dynamic> info, {required final Map<String, dynamic> info,
required final bool unlocked, required final bool unlocked}) = _$FidoStateImpl;
final bool initialized}) = _$FidoStateImpl;
_FidoState._() : super._(); _FidoState._() : super._();
factory _FidoState.fromJson(Map<String, dynamic> json) = factory _FidoState.fromJson(Map<String, dynamic> json) =
@ -193,8 +174,6 @@ abstract class _FidoState extends FidoState {
@override @override
bool get unlocked; bool get unlocked;
@override @override
bool get initialized;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith => _$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View File

@ -10,14 +10,12 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map<String, dynamic> json) =>
_$FidoStateImpl( _$FidoStateImpl(
info: json['info'] as Map<String, dynamic>, info: json['info'] as Map<String, dynamic>,
unlocked: json['unlocked'] as bool, unlocked: json['unlocked'] as bool,
initialized: json['initialized'] as bool? ?? true,
); );
Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) => Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'info': instance.info, 'info': instance.info,
'unlocked': instance.unlocked, 'unlocked': instance.unlocked,
'initialized': instance.initialized,
}; };
_$FingerprintImpl _$$FingerprintImplFromJson(Map<String, dynamic> json) => _$FingerprintImpl _$$FingerprintImplFromJson(Map<String, dynamic> json) =>

View File

@ -21,11 +21,11 @@ import '../core/state.dart';
import 'models.dart'; import 'models.dart';
final fidoStateProvider = AsyncNotifierProvider.autoDispose final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>( .family<FidoStateNotifier, FidoState?, DevicePath>(
() => throw UnimplementedError(), () => throw UnimplementedError(),
); );
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> { abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState?> {
Stream<InteractionEvent> reset(); Stream<InteractionEvent> reset();
Future<PinResult> setPin(String newPin, {String? oldPin}); Future<PinResult> setPin(String newPin, {String? oldPin});
Future<PinResult> unlock(String pin); Future<PinResult> unlock(String pin);
@ -44,11 +44,11 @@ abstract class FidoFingerprintsNotifier
} }
final credentialProvider = AsyncNotifierProvider.autoDispose final credentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>( .family<FidoCredentialsNotifier, List<FidoCredential>?, DevicePath>(
() => throw UnimplementedError(), () => throw UnimplementedError(),
); );
abstract class FidoCredentialsNotifier abstract class FidoCredentialsNotifier
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>, DevicePath> { extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>?, DevicePath> {
Future<void> deleteCredential(FidoCredential credential); Future<void> deleteCredential(FidoCredential credential);
} }

View File

@ -29,6 +29,7 @@ import '../../app/views/app_failure_page.dart';
import '../../app/views/app_list_item.dart'; import '../../app/views/app_list_item.dart';
import '../../app/views/app_page.dart'; import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart'; import '../../app/views/message_page.dart';
import '../../app/views/message_page_not_initialized.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../../widgets/list_title.dart'; import '../../widgets/list_title.dart';
@ -73,7 +74,9 @@ class FingerprintsScreen extends ConsumerWidget {
); );
}, },
data: (fidoState) { data: (fidoState) {
return fidoState.unlocked return fidoState == null
? MessagePageNotInitialized(title: l10n.s_fingerprints)
: fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState) ? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState); : _FidoLockedPage(deviceData.node, fidoState);
}); });

View File

@ -76,11 +76,11 @@ class PasskeysScreen extends ConsumerWidget {
); );
}, },
data: (fidoState) { data: (fidoState) {
return fidoState.initialized return fidoState == null
? fidoState.unlocked ? MessagePageNotInitialized(title: l10n.s_passkeys)
: fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState) ? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState) : _FidoLockedPage(deviceData.node, fidoState);
: MessagePageNotInitialized(title: l10n.s_passkeys);
}); });
} }
} }
@ -235,6 +235,10 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
} }
final credentials = data.value; final credentials = data.value;
if (credentials == null) {
return _buildLoadingPage(context);
}
if (credentials.isEmpty) { if (credentials.isEmpty) {
return MessagePage( return MessagePage(
title: l10n.s_passkeys, title: l10n.s_passkeys,

View File

@ -105,15 +105,11 @@ class OathPair with _$OathPair {
class OathState with _$OathState { class OathState with _$OathState {
const OathState._(); const OathState._();
factory OathState( factory OathState(String deviceId, Version version,
String deviceId, {required bool hasKey,
Version version, {
required bool hasKey,
required bool remembered, required bool remembered,
required bool locked, required bool locked,
required KeystoreState keystore, required KeystoreState keystore}) = _OathState;
@Default(true) bool initialized,
}) = _OathState;
int? get capacity => int? get capacity =>
version.isAtLeast(4) ? (version.isAtLeast(5, 7) ? 64 : 32) : null; version.isAtLeast(4) ? (version.isAtLeast(5, 7) ? 64 : 32) : null;

View File

@ -639,7 +639,6 @@ mixin _$OathState {
bool get remembered => throw _privateConstructorUsedError; bool get remembered => throw _privateConstructorUsedError;
bool get locked => throw _privateConstructorUsedError; bool get locked => throw _privateConstructorUsedError;
KeystoreState get keystore => throw _privateConstructorUsedError; KeystoreState get keystore => throw _privateConstructorUsedError;
bool get initialized => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -658,8 +657,7 @@ abstract class $OathStateCopyWith<$Res> {
bool hasKey, bool hasKey,
bool remembered, bool remembered,
bool locked, bool locked,
KeystoreState keystore, KeystoreState keystore});
bool initialized});
$VersionCopyWith<$Res> get version; $VersionCopyWith<$Res> get version;
} }
@ -683,7 +681,6 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState>
Object? remembered = null, Object? remembered = null,
Object? locked = null, Object? locked = null,
Object? keystore = null, Object? keystore = null,
Object? initialized = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
deviceId: null == deviceId deviceId: null == deviceId
@ -710,10 +707,6 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState>
? _value.keystore ? _value.keystore
: keystore // ignore: cast_nullable_to_non_nullable : keystore // ignore: cast_nullable_to_non_nullable
as KeystoreState, as KeystoreState,
initialized: null == initialized
? _value.initialized
: initialized // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
@ -740,8 +733,7 @@ abstract class _$$OathStateImplCopyWith<$Res>
bool hasKey, bool hasKey,
bool remembered, bool remembered,
bool locked, bool locked,
KeystoreState keystore, KeystoreState keystore});
bool initialized});
@override @override
$VersionCopyWith<$Res> get version; $VersionCopyWith<$Res> get version;
@ -764,7 +756,6 @@ class __$$OathStateImplCopyWithImpl<$Res>
Object? remembered = null, Object? remembered = null,
Object? locked = null, Object? locked = null,
Object? keystore = null, Object? keystore = null,
Object? initialized = null,
}) { }) {
return _then(_$OathStateImpl( return _then(_$OathStateImpl(
null == deviceId null == deviceId
@ -791,10 +782,6 @@ class __$$OathStateImplCopyWithImpl<$Res>
? _value.keystore ? _value.keystore
: keystore // ignore: cast_nullable_to_non_nullable : keystore // ignore: cast_nullable_to_non_nullable
as KeystoreState, as KeystoreState,
initialized: null == initialized
? _value.initialized
: initialized // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@ -806,8 +793,7 @@ class _$OathStateImpl extends _OathState {
{required this.hasKey, {required this.hasKey,
required this.remembered, required this.remembered,
required this.locked, required this.locked,
required this.keystore, required this.keystore})
this.initialized = true})
: super._(); : super._();
factory _$OathStateImpl.fromJson(Map<String, dynamic> json) => factory _$OathStateImpl.fromJson(Map<String, dynamic> json) =>
@ -825,13 +811,10 @@ class _$OathStateImpl extends _OathState {
final bool locked; final bool locked;
@override @override
final KeystoreState keystore; final KeystoreState keystore;
@override
@JsonKey()
final bool initialized;
@override @override
String toString() { String toString() {
return 'OathState(deviceId: $deviceId, version: $version, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore, initialized: $initialized)'; return 'OathState(deviceId: $deviceId, version: $version, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore)';
} }
@override @override
@ -847,15 +830,13 @@ class _$OathStateImpl extends _OathState {
other.remembered == remembered) && other.remembered == remembered) &&
(identical(other.locked, locked) || other.locked == locked) && (identical(other.locked, locked) || other.locked == locked) &&
(identical(other.keystore, keystore) || (identical(other.keystore, keystore) ||
other.keystore == keystore) && other.keystore == keystore));
(identical(other.initialized, initialized) ||
other.initialized == initialized));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash(runtimeType, deviceId, version, hasKey, int get hashCode => Object.hash(
remembered, locked, keystore, initialized); runtimeType, deviceId, version, hasKey, remembered, locked, keystore);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -876,8 +857,7 @@ abstract class _OathState extends OathState {
{required final bool hasKey, {required final bool hasKey,
required final bool remembered, required final bool remembered,
required final bool locked, required final bool locked,
required final KeystoreState keystore, required final KeystoreState keystore}) = _$OathStateImpl;
final bool initialized}) = _$OathStateImpl;
_OathState._() : super._(); _OathState._() : super._();
factory _OathState.fromJson(Map<String, dynamic> json) = factory _OathState.fromJson(Map<String, dynamic> json) =
@ -896,8 +876,6 @@ abstract class _OathState extends OathState {
@override @override
KeystoreState get keystore; KeystoreState get keystore;
@override @override
bool get initialized;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$OathStateImplCopyWith<_$OathStateImpl> get copyWith => _$$OathStateImplCopyWith<_$OathStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View File

@ -70,7 +70,6 @@ _$OathStateImpl _$$OathStateImplFromJson(Map<String, dynamic> json) =>
remembered: json['remembered'] as bool, remembered: json['remembered'] as bool,
locked: json['locked'] as bool, locked: json['locked'] as bool,
keystore: $enumDecode(_$KeystoreStateEnumMap, json['keystore']), keystore: $enumDecode(_$KeystoreStateEnumMap, json['keystore']),
initialized: json['initialized'] as bool? ?? true,
); );
Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) => Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
@ -81,7 +80,6 @@ Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
'remembered': instance.remembered, 'remembered': instance.remembered,
'locked': instance.locked, 'locked': instance.locked,
'keystore': _$KeystoreStateEnumMap[instance.keystore]!, 'keystore': _$KeystoreStateEnumMap[instance.keystore]!,
'initialized': instance.initialized,
}; };
const _$KeystoreStateEnumMap = { const _$KeystoreStateEnumMap = {

View File

@ -23,6 +23,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/shortcuts.dart'; import '../../app/shortcuts.dart';
@ -64,15 +65,14 @@ class OathScreen extends ConsumerWidget {
graphic: CircularProgressIndicator(), graphic: CircularProgressIndicator(),
delayedContent: true, delayedContent: true,
), ),
error: (error, _) => AppFailurePage( error: (error, _) => error is ErrorDataEmpty
? MessagePageNotInitialized(title: l10n.s_accounts)
: AppFailurePage(
cause: error, cause: error,
), ),
data: (oathState) => oathState.initialized data: (oathState) => oathState.locked
? oathState.locked
? _LockedView(devicePath, oathState) ? _LockedView(devicePath, oathState)
: _UnlockedView(devicePath, oathState) : _UnlockedView(devicePath, oathState));
: MessagePageNotInitialized(title: l10n.s_accounts),
);
} }
} }