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.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.
*/
@ -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
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,30 +86,9 @@ data class SessionInfo(
data class Session(
@SerialName("info")
val info: SessionInfo,
val unlocked: Boolean,
val initialized: Boolean
val unlocked: Boolean
) {
constructor(infoData: InfoData, unlocked: Boolean) : this(
SessionInfo(infoData), unlocked, true
constructor(infoData: InfoData, unlocked: Boolean) : this(
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()) {
// for NFC connections require extra tap when switching context
if (oathViewModel.sessionState.value == null) {
oathViewModel.setSessionState(Session.uninitialized)
if (oathViewModel.sessionState.value is SessionState.Empty) {
oathViewModel.setSessionState(null)
}
}
}
@ -223,7 +223,7 @@ class OathManager(
try {
device.withConnection<SmartCardConnection, Unit> { connection ->
val session = getOathSession(connection)
val previousId = oathViewModel.sessionState.value?.deviceId
val previousId = oathViewModel.currentSession()?.deviceId
if (session.deviceId == previousId && device is NfcYubiKeyDevice) {
// Run any pending action
pendingAction?.let { action ->
@ -468,7 +468,7 @@ class OathManager(
private fun forgetPassword(): String {
keyManager.clearAll()
logger.debug("Cleared all keys.")
oathViewModel.sessionState.value?.let {
oathViewModel.currentSession()?.let {
oathViewModel.setSessionState(
it.copy(
isLocked = it.isAccessKeySet,

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/logging.dart';
import '../../app/models.dart';
import '../../app/state.dart';
@ -51,6 +52,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
_sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event);
if (json == null) {
state = AsyncValue.error(const ErrorDataEmpty(), StackTrace.current);
} else if (json[0] is Map && json[0]['loading'] == true) {
state = const AsyncValue.loading();
} else {
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
.family<FidoStateNotifier, FidoState, DevicePath>(
.family<FidoStateNotifier, FidoState?, DevicePath>(
_DesktopFidoStateNotifier.new);
class _DesktopFidoStateNotifier extends FidoStateNotifier {
late RpcNodeSession _session;
late StateController<String?> _pinController;
FutureOr<FidoState> _build(DevicePath devicePath) async {
FutureOr<FidoState?> _build(DevicePath devicePath) async {
var result = await _session.command('get');
FidoState fidoState = FidoState.fromJson(result['data']);
if (fidoState.hasPin && !fidoState.unlocked) {
@ -71,7 +71,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
}
@override
FutureOr<FidoState> build(DevicePath devicePath) async {
FutureOr<FidoState?> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
if (Platform.isWindows) {
// Make sure to rebuild if isAdmin changes
@ -269,7 +269,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
}
final desktopCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
.family<FidoCredentialsNotifier, List<FidoCredential>?, DevicePath>(
_DesktopFidoCredentialsNotifier.new);
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {

View File

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

View File

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

View File

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

View File

@ -21,11 +21,11 @@ import '../core/state.dart';
import 'models.dart';
final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
.family<FidoStateNotifier, FidoState?, DevicePath>(
() => throw UnimplementedError(),
);
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState?> {
Stream<InteractionEvent> reset();
Future<PinResult> setPin(String newPin, {String? oldPin});
Future<PinResult> unlock(String pin);
@ -44,11 +44,11 @@ abstract class FidoFingerprintsNotifier
}
final credentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
.family<FidoCredentialsNotifier, List<FidoCredential>?, DevicePath>(
() => throw UnimplementedError(),
);
abstract class FidoCredentialsNotifier
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>, DevicePath> {
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>?, DevicePath> {
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_page.dart';
import '../../app/views/message_page.dart';
import '../../app/views/message_page_not_initialized.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../../widgets/list_title.dart';
@ -73,9 +74,11 @@ class FingerprintsScreen extends ConsumerWidget {
);
},
data: (fidoState) {
return fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState);
return fidoState == null
? MessagePageNotInitialized(title: l10n.s_fingerprints)
: fidoState.unlocked
? _FidoUnlockedPage(deviceData.node, fidoState)
: _FidoLockedPage(deviceData.node, fidoState);
});
}
}

View File

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

View File

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

View File

@ -70,7 +70,6 @@ _$OathStateImpl _$$OathStateImplFromJson(Map<String, dynamic> json) =>
remembered: json['remembered'] as bool,
locked: json['locked'] as bool,
keystore: $enumDecode(_$KeystoreStateEnumMap, json['keystore']),
initialized: json['initialized'] as bool? ?? true,
);
Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
@ -81,7 +80,6 @@ Map<String, dynamic> _$$OathStateImplToJson(_$OathStateImpl instance) =>
'remembered': instance.remembered,
'locked': instance.locked,
'keystore': _$KeystoreStateEnumMap[instance.keystore]!,
'initialized': instance.initialized,
};
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:material_symbols_icons/symbols.dart';
import '../../app/error_data_empty.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
@ -59,20 +60,19 @@ class OathScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return ref.watch(oathStateProvider(devicePath)).when(
loading: () => const MessagePage(
centered: true,
graphic: CircularProgressIndicator(),
delayedContent: true,
),
error: (error, _) => AppFailurePage(
cause: error,
),
data: (oathState) => oathState.initialized
? oathState.locked
? _LockedView(devicePath, oathState)
: _UnlockedView(devicePath, oathState)
: MessagePageNotInitialized(title: l10n.s_accounts),
);
loading: () => const MessagePage(
centered: true,
graphic: CircularProgressIndicator(),
delayedContent: true,
),
error: (error, _) => error is ErrorDataEmpty
? MessagePageNotInitialized(title: l10n.s_accounts)
: AppFailurePage(
cause: error,
),
data: (oathState) => oathState.locked
? _LockedView(devicePath, oathState)
: _UnlockedView(devicePath, oathState));
}
}