mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge PR #72.
This commit is contained in:
commit
0ec78278cd
@ -7,8 +7,8 @@ import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../management/state.dart';
|
||||
|
||||
final androidManagementState = StateNotifierProvider.autoDispose.family<
|
||||
ManagementStateNotifier, ApplicationStateResult<DeviceInfo>, DevicePath>(
|
||||
final androidManagementState = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
// Make sure to rebuild if currentDevice changes (as on reboot)
|
||||
ref.watch(currentDeviceProvider);
|
||||
@ -34,7 +34,7 @@ class _AndroidManagementStateNotifier extends ManagementStateNotifier {
|
||||
String newLockCode = '',
|
||||
bool reboot = false}) async {
|
||||
if (reboot) {
|
||||
unsetState();
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
|
||||
_ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
|
@ -22,7 +22,7 @@ class CancelException implements Exception {}
|
||||
final oathApiProvider = StateProvider((_) => OathApi());
|
||||
|
||||
final androidOathStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, ApplicationStateResult<OathState>, DevicePath>(
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) => _AndroidOathStateNotifier(
|
||||
ref.watch(androidStateProvider), ref.watch(oathApiProvider)));
|
||||
|
||||
@ -31,7 +31,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||
|
||||
_AndroidOathStateNotifier(OathState? newState, this._api) : super() {
|
||||
if (newState != null) {
|
||||
setState(newState);
|
||||
setData(newState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||
|
||||
if (unlockSuccess) {
|
||||
_log.config('applet unlocked');
|
||||
setState(requireState().copyWith(locked: false));
|
||||
setData(state.value!.copyWith(locked: false));
|
||||
}
|
||||
return Pair(unlockSuccess, false); // TODO: provide correct second param
|
||||
} on PlatformException catch (e) {
|
||||
@ -99,8 +99,8 @@ final androidCredentialListProvider = StateNotifierProvider.autoDispose
|
||||
var notifier = _AndroidCredentialListNotifier(
|
||||
ref.watch(oathApiProvider),
|
||||
ref.watch(androidCredentialsProvider),
|
||||
ref.watch(oathStateProvider(devicePath).select(
|
||||
(r) => r.whenOrNull(success: (state) => state.locked) ?? true)),
|
||||
ref.watch(oathStateProvider(devicePath)
|
||||
.select((r) => r.whenOrNull(data: (state) => state.locked) ?? true)),
|
||||
);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
|
@ -74,13 +74,6 @@ extension Applications on Application {
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ApplicationStateResult<T> with _$ApplicationStateResult {
|
||||
factory ApplicationStateResult.none() = _None;
|
||||
factory ApplicationStateResult.failure(String reason) = _Failure;
|
||||
factory ApplicationStateResult.success(T state) = _Success;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class YubiKeyData with _$YubiKeyData {
|
||||
factory YubiKeyData(DeviceNode node, String name, DeviceInfo info) =
|
||||
|
@ -13,482 +13,6 @@ T _$identity<T>(T value) => value;
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
class _$ApplicationStateResultTearOff {
|
||||
const _$ApplicationStateResultTearOff();
|
||||
|
||||
_None<T> none<T>() {
|
||||
return _None<T>();
|
||||
}
|
||||
|
||||
_Failure<T> failure<T>(String reason) {
|
||||
return _Failure<T>(
|
||||
reason,
|
||||
);
|
||||
}
|
||||
|
||||
_Success<T> success<T>(T state) {
|
||||
return _Success<T>(
|
||||
state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $ApplicationStateResult = _$ApplicationStateResultTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ApplicationStateResult<T> {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() none,
|
||||
required TResult Function(String reason) failure,
|
||||
required TResult Function(T state) success,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_None<T> value) none,
|
||||
required TResult Function(_Failure<T> value) failure,
|
||||
required TResult Function(_Success<T> value) success,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ApplicationStateResultCopyWith<T, $Res> {
|
||||
factory $ApplicationStateResultCopyWith(ApplicationStateResult<T> value,
|
||||
$Res Function(ApplicationStateResult<T>) then) =
|
||||
_$ApplicationStateResultCopyWithImpl<T, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ApplicationStateResultCopyWithImpl<T, $Res>
|
||||
implements $ApplicationStateResultCopyWith<T, $Res> {
|
||||
_$ApplicationStateResultCopyWithImpl(this._value, this._then);
|
||||
|
||||
final ApplicationStateResult<T> _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(ApplicationStateResult<T>) _then;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$NoneCopyWith<T, $Res> {
|
||||
factory _$NoneCopyWith(_None<T> value, $Res Function(_None<T>) then) =
|
||||
__$NoneCopyWithImpl<T, $Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$NoneCopyWithImpl<T, $Res>
|
||||
extends _$ApplicationStateResultCopyWithImpl<T, $Res>
|
||||
implements _$NoneCopyWith<T, $Res> {
|
||||
__$NoneCopyWithImpl(_None<T> _value, $Res Function(_None<T>) _then)
|
||||
: super(_value, (v) => _then(v as _None<T>));
|
||||
|
||||
@override
|
||||
_None<T> get _value => super._value as _None<T>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_None<T> implements _None<T> {
|
||||
_$_None();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApplicationStateResult<$T>.none()';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType && other is _None<T>);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() none,
|
||||
required TResult Function(String reason) failure,
|
||||
required TResult Function(T state) success,
|
||||
}) {
|
||||
return none();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
}) {
|
||||
return none?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (none != null) {
|
||||
return none();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_None<T> value) none,
|
||||
required TResult Function(_Failure<T> value) failure,
|
||||
required TResult Function(_Success<T> value) success,
|
||||
}) {
|
||||
return none(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
}) {
|
||||
return none?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (none != null) {
|
||||
return none(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _None<T> implements ApplicationStateResult<T> {
|
||||
factory _None() = _$_None<T>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$FailureCopyWith<T, $Res> {
|
||||
factory _$FailureCopyWith(
|
||||
_Failure<T> value, $Res Function(_Failure<T>) then) =
|
||||
__$FailureCopyWithImpl<T, $Res>;
|
||||
$Res call({String reason});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$FailureCopyWithImpl<T, $Res>
|
||||
extends _$ApplicationStateResultCopyWithImpl<T, $Res>
|
||||
implements _$FailureCopyWith<T, $Res> {
|
||||
__$FailureCopyWithImpl(_Failure<T> _value, $Res Function(_Failure<T>) _then)
|
||||
: super(_value, (v) => _then(v as _Failure<T>));
|
||||
|
||||
@override
|
||||
_Failure<T> get _value => super._value as _Failure<T>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? reason = freezed,
|
||||
}) {
|
||||
return _then(_Failure<T>(
|
||||
reason == freezed
|
||||
? _value.reason
|
||||
: reason // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_Failure<T> implements _Failure<T> {
|
||||
_$_Failure(this.reason);
|
||||
|
||||
@override
|
||||
final String reason;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApplicationStateResult<$T>.failure(reason: $reason)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _Failure<T> &&
|
||||
const DeepCollectionEquality().equals(other.reason, reason));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(reason));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$FailureCopyWith<T, _Failure<T>> get copyWith =>
|
||||
__$FailureCopyWithImpl<T, _Failure<T>>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() none,
|
||||
required TResult Function(String reason) failure,
|
||||
required TResult Function(T state) success,
|
||||
}) {
|
||||
return failure(reason);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
}) {
|
||||
return failure?.call(reason);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (failure != null) {
|
||||
return failure(reason);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_None<T> value) none,
|
||||
required TResult Function(_Failure<T> value) failure,
|
||||
required TResult Function(_Success<T> value) success,
|
||||
}) {
|
||||
return failure(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
}) {
|
||||
return failure?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (failure != null) {
|
||||
return failure(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Failure<T> implements ApplicationStateResult<T> {
|
||||
factory _Failure(String reason) = _$_Failure<T>;
|
||||
|
||||
String get reason;
|
||||
@JsonKey(ignore: true)
|
||||
_$FailureCopyWith<T, _Failure<T>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$SuccessCopyWith<T, $Res> {
|
||||
factory _$SuccessCopyWith(
|
||||
_Success<T> value, $Res Function(_Success<T>) then) =
|
||||
__$SuccessCopyWithImpl<T, $Res>;
|
||||
$Res call({T state});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SuccessCopyWithImpl<T, $Res>
|
||||
extends _$ApplicationStateResultCopyWithImpl<T, $Res>
|
||||
implements _$SuccessCopyWith<T, $Res> {
|
||||
__$SuccessCopyWithImpl(_Success<T> _value, $Res Function(_Success<T>) _then)
|
||||
: super(_value, (v) => _then(v as _Success<T>));
|
||||
|
||||
@override
|
||||
_Success<T> get _value => super._value as _Success<T>;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? state = freezed,
|
||||
}) {
|
||||
return _then(_Success<T>(
|
||||
state == freezed
|
||||
? _value.state
|
||||
: state // ignore: cast_nullable_to_non_nullable
|
||||
as T,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_Success<T> implements _Success<T> {
|
||||
_$_Success(this.state);
|
||||
|
||||
@override
|
||||
final T state;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApplicationStateResult<$T>.success(state: $state)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _Success<T> &&
|
||||
const DeepCollectionEquality().equals(other.state, state));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(state));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$SuccessCopyWith<T, _Success<T>> get copyWith =>
|
||||
__$SuccessCopyWithImpl<T, _Success<T>>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() none,
|
||||
required TResult Function(String reason) failure,
|
||||
required TResult Function(T state) success,
|
||||
}) {
|
||||
return success(state);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
}) {
|
||||
return success?.call(state);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? none,
|
||||
TResult Function(String reason)? failure,
|
||||
TResult Function(T state)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(state);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_None<T> value) none,
|
||||
required TResult Function(_Failure<T> value) failure,
|
||||
required TResult Function(_Success<T> value) success,
|
||||
}) {
|
||||
return success(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
}) {
|
||||
return success?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_None<T> value)? none,
|
||||
TResult Function(_Failure<T> value)? failure,
|
||||
TResult Function(_Success<T> value)? success,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Success<T> implements ApplicationStateResult<T> {
|
||||
factory _Success(T state) = _$_Success<T>;
|
||||
|
||||
T get state;
|
||||
@JsonKey(ignore: true)
|
||||
_$SuccessCopyWith<T, _Success<T>> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$YubiKeyDataTearOff {
|
||||
const _$YubiKeyDataTearOff();
|
||||
|
@ -5,8 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
|
||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
final isAndroid = Platform.isAndroid;
|
||||
|
||||
@ -28,33 +26,21 @@ class LogLevelNotifier extends StateNotifier<Level> {
|
||||
}
|
||||
|
||||
abstract class ApplicationStateNotifier<T>
|
||||
extends StateNotifier<ApplicationStateResult<T>> {
|
||||
ApplicationStateNotifier() : super(ApplicationStateResult.none());
|
||||
extends StateNotifier<AsyncValue<T>> {
|
||||
ApplicationStateNotifier() : super(const AsyncValue.loading());
|
||||
|
||||
@protected
|
||||
T requireState() => state.maybeWhen(
|
||||
success: (state) => state,
|
||||
orElse: () => throw UnsupportedError('State is not available'),
|
||||
);
|
||||
|
||||
@protected
|
||||
void setState(T value) {
|
||||
Future<void> updateState(Future<T> Function() guarded) async {
|
||||
final result = await AsyncValue.guard(guarded);
|
||||
if (mounted) {
|
||||
state = ApplicationStateResult.success(value);
|
||||
state = result;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void setFailure(String reason) {
|
||||
void setData(T value) {
|
||||
if (mounted) {
|
||||
state = ApplicationStateResult.failure(reason);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void unsetState() {
|
||||
if (mounted) {
|
||||
state = ApplicationStateResult.none();
|
||||
state = AsyncValue.data(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,22 @@ import '../state.dart';
|
||||
|
||||
final _log = Logger('desktop.fido.state');
|
||||
|
||||
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
|
||||
(ref, _) => null,
|
||||
);
|
||||
|
||||
final _sessionProvider =
|
||||
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
||||
(ref, devicePath) =>
|
||||
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['fido', 'ctap2']),
|
||||
(ref, devicePath) {
|
||||
// Make sure the pinProvider is held for the duration of the session.
|
||||
ref.watch(_pinProvider(devicePath));
|
||||
return RpcNodeSession(
|
||||
ref.watch(rpcProvider), devicePath, ['fido', 'ctap2']);
|
||||
},
|
||||
);
|
||||
|
||||
final desktopFidoState = StateNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, ApplicationStateResult<FidoState>, DevicePath>(
|
||||
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopFidoStateNotifier(session);
|
||||
@ -38,17 +46,11 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
_DesktopFidoStateNotifier(this._session) : super();
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
var result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
var fidoState = FidoState.fromJson(result['data']);
|
||||
setState(fidoState);
|
||||
} catch (error) {
|
||||
_log.severe('Unable to update FIDO state', jsonEncode(error));
|
||||
setFailure('Failed to update FIDO');
|
||||
}
|
||||
}
|
||||
Future<void> refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
return FidoState.fromJson(result['data']);
|
||||
});
|
||||
|
||||
@override
|
||||
Stream<InteractionEvent> reset() {
|
||||
@ -93,12 +95,235 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// TODO: Update state
|
||||
}
|
||||
}
|
||||
|
||||
final desktopFidoPinProvider = StateNotifierProvider.autoDispose
|
||||
.family<PinNotifier, bool, DevicePath>((ref, devicePath) {
|
||||
return _DesktopPinNotifier(ref.watch(_sessionProvider(devicePath)),
|
||||
ref.watch(_pinProvider(devicePath).notifier));
|
||||
});
|
||||
|
||||
class _DesktopPinNotifier extends PinNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pinController;
|
||||
|
||||
_DesktopPinNotifier(this._session, this._pinController)
|
||||
: super(_pinController.state != null);
|
||||
|
||||
@override
|
||||
Future<PinResult> unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'verify_pin',
|
||||
params: {'pin': pin},
|
||||
);
|
||||
_pinController.state = pin;
|
||||
|
||||
return PinResult.success();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
_pinController.state = null;
|
||||
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoFingerprintsNotifier,
|
||||
AsyncValue<List<Fingerprint>>,
|
||||
DevicePath>((ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopFidoFingerprintsNotifier(
|
||||
session,
|
||||
ref.watch(_pinProvider(devicePath).notifier),
|
||||
);
|
||||
session.setErrorHandler('auth-required', (_) async {
|
||||
final pin = ref.read(_pinProvider(devicePath));
|
||||
if (pin != null) {
|
||||
await notifier._unlock(pin);
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unsetErrorHandler('auth-required');
|
||||
});
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pinNotifier;
|
||||
|
||||
_DesktopFidoFingerprintsNotifier(this._session, this._pinNotifier) {
|
||||
final pin = _pinNotifier.state;
|
||||
if (pin != null) {
|
||||
_unlock(pin);
|
||||
} else {
|
||||
state = const AsyncValue.error('locked');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'unlock',
|
||||
target: ['fingerprints'],
|
||||
params: {'pin': pin},
|
||||
);
|
||||
await _refresh();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
_pinNotifier.state = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final result = await _session.command('fingerprints');
|
||||
setItems((result['children'] as Map<String, dynamic>)
|
||||
.entries
|
||||
.map((e) => Fingerprint(e.key, e.value['name']))
|
||||
.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PinResult> unlock(String pin) {
|
||||
// TODO: implement unlock
|
||||
throw UnimplementedError();
|
||||
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
||||
await _session
|
||||
.command('delete', target: ['fingerprints', fingerprint.templateId]);
|
||||
await _refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<FingerprintEvent> registerFingerprint({String? name}) {
|
||||
final controller = StreamController<FingerprintEvent>();
|
||||
final signaler = Signaler();
|
||||
signaler.signals.listen((signal) {
|
||||
switch (signal.status) {
|
||||
case 'capture':
|
||||
controller.sink
|
||||
.add(FingerprintEvent.capture(signal.body['remaining']));
|
||||
break;
|
||||
case 'capture-error':
|
||||
controller.sink.add(FingerprintEvent.error(signal.body['code']));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
controller.onCancel = () {
|
||||
if (!controller.isClosed) {
|
||||
signaler.cancel();
|
||||
}
|
||||
};
|
||||
controller.onListen = () async {
|
||||
try {
|
||||
final result = await _session.command(
|
||||
'add',
|
||||
target: ['fingerprints'],
|
||||
params: {'name': name},
|
||||
signal: signaler,
|
||||
);
|
||||
controller.sink
|
||||
.add(FingerprintEvent.complete(Fingerprint.fromJson(result)));
|
||||
await _refresh();
|
||||
await controller.sink.close();
|
||||
} catch (e) {
|
||||
controller.sink.addError(e);
|
||||
}
|
||||
};
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Fingerprint> renameFingerprint(
|
||||
Fingerprint fingerprint, String name) async {
|
||||
await _session.command('rename',
|
||||
target: ['fingerprints', fingerprint.templateId],
|
||||
params: {'name': name});
|
||||
final renamed = fingerprint.copyWith(name: name);
|
||||
await _refresh();
|
||||
return renamed;
|
||||
}
|
||||
}
|
||||
|
||||
final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoCredentialsNotifier,
|
||||
AsyncValue<List<FidoCredential>>,
|
||||
DevicePath>((ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopFidoCredentialsNotifier(
|
||||
session,
|
||||
ref.watch(_pinProvider(devicePath).notifier),
|
||||
);
|
||||
session.setErrorHandler('auth-required', (_) async {
|
||||
final pin = ref.read(_pinProvider(devicePath));
|
||||
if (pin != null) {
|
||||
await notifier._unlock(pin);
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unsetErrorHandler('auth-required');
|
||||
});
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pinNotifier;
|
||||
|
||||
_DesktopFidoCredentialsNotifier(this._session, this._pinNotifier) {
|
||||
final pin = _pinNotifier.state;
|
||||
if (pin != null) {
|
||||
_unlock(pin);
|
||||
} else {
|
||||
state = const AsyncValue.error('locked');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'unlock',
|
||||
target: ['credentials'],
|
||||
params: {'pin': pin},
|
||||
);
|
||||
await _refresh();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
_pinNotifier.state = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final List<FidoCredential> creds = [];
|
||||
final rps = await _session.command('credentials');
|
||||
for (final rpId in (rps['children'] as Map<String, dynamic>).keys) {
|
||||
final result = await _session.command(rpId, target: ['credentials']);
|
||||
for (final e in (result['children'] as Map<String, dynamic>).entries) {
|
||||
creds.add(FidoCredential(
|
||||
rpId: rpId,
|
||||
credentialId: e.key,
|
||||
userId: e.value['user_id'],
|
||||
userName: e.value['user_name']));
|
||||
}
|
||||
}
|
||||
setItems(creds);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteCredential(FidoCredential credential) async {
|
||||
await _session.command('delete', target: [
|
||||
'credentials',
|
||||
credential.rpId,
|
||||
credential.credentialId,
|
||||
]);
|
||||
await _refresh();
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,9 @@ Future<List<Override>> initializeAndGetOverrides(
|
||||
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
||||
managementStateProvider.overrideWithProvider(desktopManagementState),
|
||||
fidoStateProvider.overrideWithProvider(desktopFidoState),
|
||||
fidoPinProvider.overrideWithProvider(desktopFidoPinProvider),
|
||||
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
||||
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
||||
currentDeviceProvider.overrideWithProvider(desktopCurrentDeviceProvider)
|
||||
];
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ final _sessionProvider =
|
||||
(ref, devicePath) => RpcNodeSession(ref.watch(rpcProvider), devicePath, []),
|
||||
);
|
||||
|
||||
final desktopManagementState = StateNotifierProvider.autoDispose.family<
|
||||
ManagementStateNotifier, ApplicationStateResult<DeviceInfo>, DevicePath>(
|
||||
final desktopManagementState = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
// Make sure to rebuild if currentDevice changes (as on reboot)
|
||||
ref.watch(currentDeviceProvider);
|
||||
@ -41,36 +41,30 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
List<String> _subpath = [];
|
||||
_DesktopManagementStateNotifier(this._ref, this._session) : super();
|
||||
|
||||
void refresh() async {
|
||||
try {
|
||||
final result = await _session.command('get');
|
||||
final info = DeviceInfo.fromJson(result['data']['info']);
|
||||
final interfaces = (result['children'] as Map).keys.toSet();
|
||||
for (final iface in [
|
||||
// This is the preferred order
|
||||
UsbInterface.ccid,
|
||||
UsbInterface.otp,
|
||||
UsbInterface.fido,
|
||||
]) {
|
||||
if (interfaces.contains(iface.name)) {
|
||||
final path = [iface.name, 'management'];
|
||||
try {
|
||||
await _session.command('get', target: path);
|
||||
_subpath = path;
|
||||
_log.config('Using transport $iface for management');
|
||||
setState(info);
|
||||
return;
|
||||
} catch (e) {
|
||||
_log.warning('Failed connecting to management via $iface');
|
||||
Future<void> refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
final info = DeviceInfo.fromJson(result['data']['info']);
|
||||
final interfaces = (result['children'] as Map).keys.toSet();
|
||||
for (final iface in [
|
||||
// This is the preferred order
|
||||
UsbInterface.ccid,
|
||||
UsbInterface.otp,
|
||||
UsbInterface.fido,
|
||||
]) {
|
||||
if (interfaces.contains(iface.name)) {
|
||||
final path = [iface.name, 'management'];
|
||||
try {
|
||||
await _session.command('get', target: path);
|
||||
_subpath = path;
|
||||
_log.config('Using transport $iface for management');
|
||||
return info;
|
||||
} catch (e) {
|
||||
_log.warning('Failed connecting to management via $iface');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setFailure('Failed connecting over all interfaces');
|
||||
} catch (error) {
|
||||
_log.severe('Failed getting device info');
|
||||
setFailure('Failed to connect');
|
||||
}
|
||||
}
|
||||
throw 'Failed connection over all interfaces';
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> setMode(int mode,
|
||||
@ -88,7 +82,7 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
String newLockCode = '',
|
||||
bool reboot = false}) async {
|
||||
if (reboot) {
|
||||
unsetState();
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
await _session.command('configure', target: _subpath, params: {
|
||||
...config.toJson(),
|
||||
|
@ -44,7 +44,7 @@ class _LockKeyNotifier extends StateNotifier<String?> {
|
||||
}
|
||||
|
||||
final desktopOathState = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, ApplicationStateResult<OathState>, DevicePath>(
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopOathStateNotifier(session, ref);
|
||||
@ -69,28 +69,24 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
final Ref _ref;
|
||||
_DesktopOathStateNotifier(this._session, this._ref) : super();
|
||||
|
||||
refresh() async {
|
||||
try {
|
||||
var result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _ref.read(_oathLockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result = await _session.command('validate', params: {'key': key});
|
||||
if (result['valid']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_ref
|
||||
.read(_oathLockKeyProvider(_session.devicePath).notifier)
|
||||
.unsetKey();
|
||||
refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _ref.read(_oathLockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result =
|
||||
await _session.command('validate', params: {'key': key});
|
||||
if (result['valid']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_ref
|
||||
.read(_oathLockKeyProvider(_session.devicePath).notifier)
|
||||
.unsetKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(oathState);
|
||||
} catch (error) {
|
||||
_log.severe('Unable to update OATH state', jsonEncode(error));
|
||||
setFailure('Failed to update OATH');
|
||||
}
|
||||
}
|
||||
return oathState;
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> reset() async {
|
||||
@ -112,7 +108,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
if (valid) {
|
||||
_log.config('applet unlocked');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
setState(requireState().copyWith(
|
||||
setData(state.value!.copyWith(
|
||||
locked: false,
|
||||
remembered: remembered,
|
||||
));
|
||||
@ -128,7 +124,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
|
||||
@override
|
||||
Future<bool> setPassword(String? current, String password) async {
|
||||
final oathState = requireState();
|
||||
final oathState = state.value!;
|
||||
if (oathState.hasKey) {
|
||||
if (current != null) {
|
||||
if (!await _checkPassword(current)) {
|
||||
@ -154,14 +150,14 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
_log.config('OATH key set');
|
||||
|
||||
if (!oathState.hasKey) {
|
||||
setState(oathState.copyWith(hasKey: true));
|
||||
setData(oathState.copyWith(hasKey: true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> unsetPassword(String current) async {
|
||||
final oathState = requireState();
|
||||
final oathState = state.value!;
|
||||
if (oathState.hasKey) {
|
||||
if (!await _checkPassword(current)) {
|
||||
return false;
|
||||
@ -169,7 +165,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
}
|
||||
await _session.command('unset_key');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
setState(oathState.copyWith(hasKey: false, locked: false));
|
||||
setData(oathState.copyWith(hasKey: false, locked: false));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -177,7 +173,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
Future<void> forgetPassword() async {
|
||||
await _session.command('forget');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
setState(requireState().copyWith(remembered: false));
|
||||
setData(state.value!.copyWith(remembered: false));
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,8 +182,8 @@ final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose
|
||||
(ref, devicePath) {
|
||||
var notifier = _DesktopCredentialListNotifier(
|
||||
ref.watch(_sessionProvider(devicePath)),
|
||||
ref.watch(oathStateProvider(devicePath).select(
|
||||
(r) => r.whenOrNull(success: (state) => state.locked) ?? true)),
|
||||
ref.watch(oathStateProvider(devicePath)
|
||||
.select((r) => r.whenOrNull(data: (state) => state.locked) ?? true)),
|
||||
);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
|
@ -16,6 +16,8 @@ class Signaler {
|
||||
|
||||
Stream<Signal> get signals => _recv.stream;
|
||||
|
||||
Stream<String> get _sendStream => _send.stream;
|
||||
|
||||
void cancel() {
|
||||
_send.add('cancel');
|
||||
}
|
||||
@ -122,14 +124,13 @@ class RpcSession {
|
||||
void _send(Map data) {
|
||||
_log.fine('SEND', jsonEncode(data));
|
||||
_process.stdin.writeln(jsonEncode(data));
|
||||
_process.stdin.flush();
|
||||
}
|
||||
|
||||
void _pump() async {
|
||||
await for (final request in _requests.stream) {
|
||||
_send(request.toJson());
|
||||
|
||||
request.signal?._send.stream.listen((status) {
|
||||
final signalSubscription = request.signal?._sendStream.listen((status) {
|
||||
_send({'kind': 'signal', 'status': status});
|
||||
});
|
||||
|
||||
@ -157,6 +158,7 @@ class RpcSession {
|
||||
);
|
||||
}
|
||||
|
||||
await signalSubscription?.cancel();
|
||||
request.signal?._close();
|
||||
}
|
||||
}
|
||||
@ -164,6 +166,24 @@ class RpcSession {
|
||||
|
||||
typedef ErrorHandler = Future<void> Function(RpcError e);
|
||||
|
||||
class _MultiSignaler extends Signaler {
|
||||
final Signaler delegate;
|
||||
@override
|
||||
final Stream<String> _sendStream;
|
||||
|
||||
_MultiSignaler(this.delegate)
|
||||
: _sendStream = delegate._send.stream.asBroadcastStream() {
|
||||
signals.listen(delegate._recv.sink.add);
|
||||
}
|
||||
|
||||
@override
|
||||
void _close() {}
|
||||
|
||||
void _reallyClose() {
|
||||
super._close();
|
||||
}
|
||||
}
|
||||
|
||||
class RpcNodeSession {
|
||||
final RpcSession _rpc;
|
||||
final DevicePath devicePath;
|
||||
@ -186,7 +206,12 @@ class RpcNodeSession {
|
||||
Map<dynamic, dynamic>? params,
|
||||
Signaler? signal,
|
||||
}) async {
|
||||
bool wrapped = false;
|
||||
try {
|
||||
if (signal != null && signal is! _MultiSignaler) {
|
||||
signal = _MultiSignaler(signal);
|
||||
wrapped = true;
|
||||
}
|
||||
return await _rpc.command(
|
||||
action,
|
||||
devicePath.segments + subPath + target,
|
||||
@ -198,9 +223,18 @@ class RpcNodeSession {
|
||||
if (handler != null) {
|
||||
_log.info('Attempting recovery on "${e.status}"');
|
||||
await handler(e);
|
||||
return command(action, target: target, params: params, signal: signal);
|
||||
return await command(
|
||||
action,
|
||||
target: target,
|
||||
params: params,
|
||||
signal: signal,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
} finally {
|
||||
if (wrapped) {
|
||||
(signal as _MultiSignaler)._reallyClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,13 @@ part 'models.g.dart';
|
||||
|
||||
enum InteractionEvent { remove, insert, touch }
|
||||
|
||||
enum SubPage { main, fingerprints, credentials }
|
||||
|
||||
@freezed
|
||||
class FidoState with _$FidoState {
|
||||
const FidoState._();
|
||||
|
||||
factory FidoState({
|
||||
required Map<String, dynamic> info,
|
||||
required bool locked,
|
||||
}) = _FidoState;
|
||||
factory FidoState({required Map<String, dynamic> info}) = _FidoState;
|
||||
|
||||
factory FidoState.fromJson(Map<String, dynamic> json) =>
|
||||
_$FidoStateFromJson(json);
|
||||
@ -30,6 +29,37 @@ class FidoState with _$FidoState {
|
||||
|
||||
@freezed
|
||||
class PinResult with _$PinResult {
|
||||
factory PinResult.success() = _Success;
|
||||
factory PinResult.failed(int retries, bool authBlocked) = _Failure;
|
||||
factory PinResult.success() = _PinSuccess;
|
||||
factory PinResult.failed(int retries, bool authBlocked) = _PinFailure;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Fingerprint with _$Fingerprint {
|
||||
const Fingerprint._();
|
||||
factory Fingerprint(String templateId, String? name) = _Fingerprint;
|
||||
|
||||
factory Fingerprint.fromJson(Map<String, dynamic> json) =>
|
||||
_$FingerprintFromJson(json);
|
||||
|
||||
String get label => name ?? 'Unnamed (ID: $templateId)';
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FingerprintEvent with _$FingerprintEvent {
|
||||
factory FingerprintEvent.capture(int remaining) = _EventCapture;
|
||||
factory FingerprintEvent.complete(Fingerprint fingerprint) = _EventComplete;
|
||||
factory FingerprintEvent.error(int code) = _EventError;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FidoCredential with _$FidoCredential {
|
||||
factory FidoCredential({
|
||||
required String rpId,
|
||||
required String credentialId,
|
||||
required String userId,
|
||||
required String userName,
|
||||
}) = _FidoCredential;
|
||||
|
||||
factory FidoCredential.fromJson(Map<String, dynamic> json) =>
|
||||
_$FidoCredentialFromJson(json);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,11 +8,37 @@ part of 'models.dart';
|
||||
|
||||
_$_FidoState _$$_FidoStateFromJson(Map<String, dynamic> json) => _$_FidoState(
|
||||
info: json['info'] as Map<String, dynamic>,
|
||||
locked: json['locked'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FidoStateToJson(_$_FidoState instance) =>
|
||||
<String, dynamic>{
|
||||
'info': instance.info,
|
||||
'locked': instance.locked,
|
||||
};
|
||||
|
||||
_$_Fingerprint _$$_FingerprintFromJson(Map<String, dynamic> json) =>
|
||||
_$_Fingerprint(
|
||||
json['template_id'] as String,
|
||||
json['name'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FingerprintToJson(_$_Fingerprint instance) =>
|
||||
<String, dynamic>{
|
||||
'template_id': instance.templateId,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
_$_FidoCredential _$$_FidoCredentialFromJson(Map<String, dynamic> json) =>
|
||||
_$_FidoCredential(
|
||||
rpId: json['rp_id'] as String,
|
||||
credentialId: json['credential_id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
userName: json['user_name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FidoCredentialToJson(_$_FidoCredential instance) =>
|
||||
<String, dynamic>{
|
||||
'rp_id': instance.rpId,
|
||||
'credential_id': instance.credentialId,
|
||||
'user_id': instance.userId,
|
||||
'user_name': instance.userName,
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
@ -5,12 +6,55 @@ import '../core/state.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final fidoStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, ApplicationStateResult<FidoState>, DevicePath>(
|
||||
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||
Stream<InteractionEvent> reset();
|
||||
Future<PinResult> unlock(String pin);
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||
}
|
||||
|
||||
final fidoPinProvider =
|
||||
StateNotifierProvider.autoDispose.family<PinNotifier, bool, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class PinNotifier extends StateNotifier<bool> {
|
||||
PinNotifier(bool unlocked) : super(unlocked);
|
||||
Future<PinResult> unlock(String pin);
|
||||
}
|
||||
|
||||
abstract class LockedCollectionNotifier<T>
|
||||
extends StateNotifier<AsyncValue<List<T>>> {
|
||||
LockedCollectionNotifier() : super(const AsyncValue.loading());
|
||||
|
||||
@protected
|
||||
void setItems(List<T> items) {
|
||||
if (mounted) {
|
||||
state = AsyncValue.data(List.unmodifiable(items));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final fingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoFingerprintsNotifier, AsyncValue<List<Fingerprint>>, DevicePath>(
|
||||
(ref, arg) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class FidoFingerprintsNotifier
|
||||
extends LockedCollectionNotifier<Fingerprint> {
|
||||
Stream<FingerprintEvent> registerFingerprint({String? name});
|
||||
Future<Fingerprint> renameFingerprint(Fingerprint fingerprint, String name);
|
||||
Future<void> deleteFingerprint(Fingerprint fingerprint);
|
||||
}
|
||||
|
||||
final credentialProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoCredentialsNotifier, AsyncValue<List<FidoCredential>>, DevicePath>(
|
||||
(ref, arg) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class FidoCredentialsNotifier
|
||||
extends LockedCollectionNotifier<FidoCredential> {
|
||||
Future<void> deleteCredential(FidoCredential credential);
|
||||
}
|
||||
|
196
lib/fido/views/add_fingerprint_dialog.dart
Executable file
196
lib/fido/views/add_fingerprint_dialog.dart
Executable file
@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../state.dart';
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../../fido/models.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
|
||||
final _log = Logger('fido.views.add_fingerprint_dialog');
|
||||
|
||||
class AddFingerprintDialog extends ConsumerStatefulWidget {
|
||||
final DeviceNode node;
|
||||
const AddFingerprintDialog(this.node, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_AddFingerprintDialogState();
|
||||
}
|
||||
|
||||
class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late FocusNode _nameFocus;
|
||||
|
||||
late AnimationController _animator;
|
||||
late Animation<Color?> _color;
|
||||
late StreamSubscription<FingerprintEvent> _subscription;
|
||||
|
||||
int _samples = 0;
|
||||
int _remaining = 5;
|
||||
Fingerprint? _fingerprint;
|
||||
String _label = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animator.dispose();
|
||||
_nameFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Animation<Color?> _animateColor(Color color,
|
||||
{Function? atPeak, bool reverse = true}) {
|
||||
final animation =
|
||||
ColorTween(begin: Colors.black, end: color).animate(_animator);
|
||||
_animator.forward().then((_) {
|
||||
if (reverse) {
|
||||
atPeak?.call();
|
||||
_animator.reverse();
|
||||
}
|
||||
});
|
||||
return animation;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_nameFocus = FocusNode();
|
||||
_animator = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 250));
|
||||
_color =
|
||||
ColorTween(begin: Colors.black, end: Colors.black).animate(_animator);
|
||||
|
||||
_subscription = ref
|
||||
.read(fingerprintProvider(widget.node.path).notifier)
|
||||
.registerFingerprint()
|
||||
.listen((event) {
|
||||
setState(() {
|
||||
event.when(capture: (remaining) {
|
||||
_color = _animateColor(Colors.lightGreenAccent, atPeak: () {
|
||||
setState(() {
|
||||
_samples += 1;
|
||||
_remaining = remaining;
|
||||
});
|
||||
}, reverse: remaining > 0);
|
||||
}, complete: (fingerprint) {
|
||||
_remaining = 0;
|
||||
_fingerprint = fingerprint;
|
||||
// This needs a short delay to ensure the field is enabled first
|
||||
Timer(const Duration(milliseconds: 100), _nameFocus.requestFocus);
|
||||
}, error: (code) {
|
||||
_log.config('Fingerprint capture error (code: $code)');
|
||||
_color = _animateColor(Colors.redAccent);
|
||||
});
|
||||
});
|
||||
}, onError: (error, stacktrace) {
|
||||
_log.severe('Error adding fingerprint', error, stacktrace);
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Error adding fingerprint'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _getMessage() {
|
||||
if (_samples == 0) {
|
||||
return 'Press your finger against the YubiKey to begin.';
|
||||
}
|
||||
if (_fingerprint == null) {
|
||||
return 'Keep touching your YubiKey repeatedly...';
|
||||
} else {
|
||||
return 'Fingerprint captured successfully!';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
// Prevent over-popping if reset causes currentDevice to change.
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
final progress = _samples == 0 ? 0.0 : _samples / (_samples + _remaining);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Add fingerprint'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Step 1/2: Capture fingerprint'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _color,
|
||||
builder: (context, _) {
|
||||
return Icon(
|
||||
_fingerprint == null ? Icons.fingerprint : Icons.check,
|
||||
size: 200.0,
|
||||
color: _color.value,
|
||||
);
|
||||
},
|
||||
),
|
||||
LinearProgressIndicator(value: progress),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(_getMessage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('Step 2/2: Name fingerprint'),
|
||||
TextFormField(
|
||||
focusNode: _nameFocus,
|
||||
maxLength: 15,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
enabled: _fingerprint != null,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Name',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_label = value.trim();
|
||||
});
|
||||
},
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
onCancel: () {
|
||||
_subscription.cancel();
|
||||
},
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _fingerprint != null && _label.isNotEmpty
|
||||
? () async {
|
||||
await ref
|
||||
.read(fingerprintProvider(widget.node.path).notifier)
|
||||
.renameFingerprint(_fingerprint!, _label);
|
||||
Navigator.of(context).pop(true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fingerprint added'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
51
lib/fido/views/credential_page.dart
Executable file
51
lib/fido/views/credential_page.dart
Executable file
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'delete_credential_dialog.dart';
|
||||
|
||||
class CredentialPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final FidoState state;
|
||||
|
||||
const CredentialPage(this.node, this.state, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(credentialProvider(node.path)).when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (credentials) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'CREDENTIALS',
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
),
|
||||
),
|
||||
...credentials.map((cred) => ListTile(
|
||||
title: Text(cred.userName),
|
||||
subtitle: Text(cred.rpId),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
DeleteCredentialDialog(node, cred),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
58
lib/fido/views/delete_credential_dialog.dart
Executable file
58
lib/fido/views/delete_credential_dialog.dart
Executable file
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
|
||||
class DeleteCredentialDialog extends ConsumerWidget {
|
||||
final DeviceNode device;
|
||||
final FidoCredential credential;
|
||||
const DeleteCredentialDialog(this.device, this.credential, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop(false);
|
||||
});
|
||||
|
||||
final label = credential.userName;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Delete credential'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('This will delete the credential from your YubiKey.'),
|
||||
Text('Credential: $label'),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(credentialProvider(device.path).notifier)
|
||||
.deleteCredential(credential);
|
||||
Navigator.of(context).pop(true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Credential deleted'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
58
lib/fido/views/delete_fingerprint_dialog.dart
Executable file
58
lib/fido/views/delete_fingerprint_dialog.dart
Executable file
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
|
||||
class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
final DeviceNode device;
|
||||
final Fingerprint fingerprint;
|
||||
const DeleteFingerprintDialog(this.device, this.fingerprint, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop(false);
|
||||
});
|
||||
|
||||
final label = fingerprint.label;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Delete fingerprint'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('This will delete the fingerprint from your YubiKey.'),
|
||||
Text('Fingerprint: $label'),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(fingerprintProvider(device.path).notifier)
|
||||
.deleteFingerprint(fingerprint);
|
||||
Navigator.of(context).pop(true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fingerprint deleted'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -4,13 +4,22 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../../app/views/app_loading_screen.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'pin_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
import 'credential_page.dart';
|
||||
import 'fingerprint_page.dart';
|
||||
import 'main_page.dart';
|
||||
|
||||
final _subPageProvider = StateProvider<SubPage>((ref) {
|
||||
// Reset whenever the device changes.
|
||||
ref.watch(currentDeviceProvider);
|
||||
return SubPage.main;
|
||||
});
|
||||
|
||||
class FidoScreen extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
@ -19,8 +28,8 @@ class FidoScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||
none: () => const AppLoadingScreen(),
|
||||
failure: (reason) {
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) {
|
||||
final supported = deviceData
|
||||
.info.supportedCapabilities[deviceData.node.transport]!;
|
||||
if (Capability.fido2.value & supported == 0) {
|
||||
@ -34,56 +43,51 @@ class FidoScreen extends ConsumerWidget {
|
||||
'WebAuthn management requires elevated privileges.\nRestart this app as administrator.');
|
||||
}
|
||||
}
|
||||
return AppFailureScreen(reason);
|
||||
return AppFailureScreen('$error');
|
||||
},
|
||||
success: (state) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.pin),
|
||||
),
|
||||
title: const Text('PIN'),
|
||||
subtitle:
|
||||
Text(state.hasPin ? 'Change your PIN' : 'Set a PIN'),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FidoPinDialog(deviceData.node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.bioEnroll != null)
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.fingerprint),
|
||||
),
|
||||
title: const Text('Fingerprints'),
|
||||
subtitle: Text(state.bioEnroll == true
|
||||
? 'Fingerprints have been registered'
|
||||
: 'No fingerprints registered'),
|
||||
),
|
||||
if (state.credMgmt)
|
||||
const ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: Icon(Icons.account_box),
|
||||
),
|
||||
title: Text('Credentials'),
|
||||
subtitle: Text('Manage stored credentials on key'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.delete_forever),
|
||||
),
|
||||
title: const Text('Factory reset'),
|
||||
subtitle: const Text('Delete all data and remove PIN'),
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(deviceData.node),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
data: (state) {
|
||||
setSubPage(value) {
|
||||
ref.read(_subPageProvider.notifier).state = value;
|
||||
}
|
||||
|
||||
switch (ref.watch(_subPageProvider)) {
|
||||
case SubPage.fingerprints:
|
||||
return WithBackButton(
|
||||
goBack: () {
|
||||
setSubPage(SubPage.main);
|
||||
},
|
||||
child: FingerprintPage(deviceData.node, state),
|
||||
);
|
||||
case SubPage.credentials:
|
||||
return WithBackButton(
|
||||
goBack: () {
|
||||
setSubPage(SubPage.main);
|
||||
},
|
||||
child: CredentialPage(deviceData.node, state),
|
||||
);
|
||||
default:
|
||||
return FidoMainPage(
|
||||
deviceData.node,
|
||||
state,
|
||||
setSubPage: setSubPage,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Replace this with the AppBar back button
|
||||
class WithBackButton extends StatelessWidget {
|
||||
final Function() goBack;
|
||||
final Widget child;
|
||||
const WithBackButton({Key? key, required this.goBack, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(onPressed: goBack, child: const Text('Back')),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
78
lib/fido/views/fingerprint_page.dart
Executable file
78
lib/fido/views/fingerprint_page.dart
Executable file
@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
import 'delete_fingerprint_dialog.dart';
|
||||
import 'rename_fingerprint_dialog.dart';
|
||||
|
||||
class FingerprintPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final FidoState state;
|
||||
|
||||
const FingerprintPage(this.node, this.state, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(fingerprintProvider(node.path)).when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (fingerprints) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'FINGERPRINTS',
|
||||
style: Theme.of(context).textTheme.bodyText2,
|
||||
),
|
||||
),
|
||||
...fingerprints.map((fp) => ListTile(
|
||||
title: Text(fp.label),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
RenameFingerprintDialog(node, fp),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
DeleteFingerprintDialog(node, fp),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
label: const Text('Add fingerprint'),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddFingerprintDialog(node),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
93
lib/fido/views/main_page.dart
Executable file
93
lib/fido/views/main_page.dart
Executable file
@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'pin_dialog.dart';
|
||||
import 'pin_entry_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
|
||||
class FidoMainPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final FidoState state;
|
||||
final Function(SubPage page) setSubPage;
|
||||
|
||||
const FidoMainPage(this.node, this.state,
|
||||
{required this.setSubPage, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
_openLockedPage(BuildContext context, WidgetRef ref, SubPage subPage) async {
|
||||
final unlocked = ref.read(fidoPinProvider(node.path));
|
||||
if (unlocked) {
|
||||
setSubPage(subPage);
|
||||
} else {
|
||||
final result = await showDialog(
|
||||
context: context, builder: (context) => PinEntryDialog(node.path));
|
||||
if (result == true) {
|
||||
setSubPage(subPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.pin),
|
||||
),
|
||||
title: const Text('PIN'),
|
||||
subtitle: Text(state.hasPin ? 'Change your PIN' : 'Set a PIN'),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.bioEnroll != null)
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.fingerprint),
|
||||
),
|
||||
title: const Text('Fingerprints'),
|
||||
subtitle: Text(state.bioEnroll == true
|
||||
? 'Fingerprints have been registered'
|
||||
: 'No fingerprints registered'),
|
||||
onTap: () {
|
||||
_openLockedPage(context, ref, SubPage.fingerprints);
|
||||
},
|
||||
),
|
||||
if (state.credMgmt)
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.account_box),
|
||||
),
|
||||
title: const Text('Credentials'),
|
||||
enabled: state.hasPin,
|
||||
subtitle: Text(state.hasPin
|
||||
? 'Manage stored credentials on key'
|
||||
: 'Set a PIN to manage credentials'),
|
||||
onTap: () {
|
||||
_openLockedPage(context, ref, SubPage.credentials);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.delete_forever),
|
||||
),
|
||||
title: const Text('Factory reset'),
|
||||
subtitle: const Text('Delete all data and remove PIN'),
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
93
lib/fido/views/pin_entry_dialog.dart
Executable file
93
lib/fido/views/pin_entry_dialog.dart
Executable file
@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../state.dart';
|
||||
|
||||
class PinEntryDialog extends ConsumerStatefulWidget {
|
||||
final DevicePath _devicePath;
|
||||
const PinEntryDialog(this._devicePath, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<PinEntryDialog> createState() => _PinEntryDialogState();
|
||||
}
|
||||
|
||||
class _PinEntryDialogState extends ConsumerState<PinEntryDialog> {
|
||||
final _pinController = TextEditingController();
|
||||
bool _blocked = false;
|
||||
int? _retries;
|
||||
|
||||
void _submit() async {
|
||||
final result = await ref
|
||||
.read(fidoPinProvider(widget._devicePath).notifier)
|
||||
.unlock(_pinController.text);
|
||||
result.when(success: () {
|
||||
Navigator.pop(context, true);
|
||||
}, failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_retries = retries;
|
||||
_blocked = authBlocked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String? _getErrorText() {
|
||||
if (_retries == 0) {
|
||||
return 'PIN is blocked. Factory reset the FIDO application.';
|
||||
}
|
||||
if (_blocked) {
|
||||
return 'PIN temporarily blocked, remove and reinsert your YubiKey.';
|
||||
}
|
||||
if (_retries != null) {
|
||||
return 'Wrong PIN. $_retries attempts remaining.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Enter PIN'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Enter the FIDO PIN for your YubiKey.',
|
||||
),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: _pinController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'PIN',
|
||||
errorText: _getErrorText(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Continue'),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
88
lib/fido/views/rename_fingerprint_dialog.dart
Executable file
88
lib/fido/views/rename_fingerprint_dialog.dart
Executable file
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
|
||||
class RenameFingerprintDialog extends ConsumerStatefulWidget {
|
||||
final DeviceNode device;
|
||||
final Fingerprint fingerprint;
|
||||
const RenameFingerprintDialog(this.device, this.fingerprint, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_RenameAccountDialogState();
|
||||
}
|
||||
|
||||
class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
late String _label;
|
||||
_RenameAccountDialogState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_label = widget.fingerprint.label;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
final fingerprint = widget.fingerprint;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Rename fingerprint'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Rename ${fingerprint.label}?'),
|
||||
const Text('This will change the label of the fingerprint.'),
|
||||
TextFormField(
|
||||
initialValue: _label,
|
||||
maxLength: 15,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Label',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_label = value.trim();
|
||||
});
|
||||
},
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _label.isNotEmpty
|
||||
? () async {
|
||||
final renamed = await ref
|
||||
.read(fingerprintProvider(widget.device.path).notifier)
|
||||
.renameFingerprint(fingerprint, _label);
|
||||
Navigator.of(context).pop(renamed);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fingerprint renamed'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -4,8 +4,8 @@ import 'package:yubico_authenticator/management/models.dart';
|
||||
import '../app/models.dart';
|
||||
import '../core/state.dart';
|
||||
|
||||
final managementStateProvider = StateNotifierProvider.autoDispose.family<
|
||||
ManagementStateNotifier, ApplicationStateResult<DeviceInfo>, DevicePath>(
|
||||
final managementStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
|
@ -240,9 +240,9 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
title: const Text('Toggle applications'),
|
||||
child:
|
||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||
none: () => const AppLoadingScreen(),
|
||||
failure: (reason) => AppFailureScreen(reason),
|
||||
success: (info) {
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (info) {
|
||||
// TODO: Check mode for < YK5 intead
|
||||
changed = !_mapEquals(
|
||||
_enabled,
|
||||
|
@ -13,7 +13,7 @@ List<MenuAction> buildOathMenuActions(AutoDisposeProviderRef ref) {
|
||||
if (device != null) {
|
||||
final state = ref.watch(oathStateProvider(device.path));
|
||||
return state.whenOrNull(
|
||||
success: (oathState) => [
|
||||
data: (oathState) => [
|
||||
if (!oathState.locked) ...[
|
||||
MenuAction(
|
||||
text: 'Add credential',
|
||||
|
@ -11,7 +11,7 @@ import '../core/state.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final oathStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, ApplicationStateResult<OathState>, DevicePath>(
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
|
@ -15,9 +15,9 @@ class OathScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(oathStateProvider(deviceData.node.path)).when(
|
||||
none: () => const AppLoadingScreen(),
|
||||
failure: (reason) => AppFailureScreen(reason),
|
||||
success: (oathState) {
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (oathState) {
|
||||
if (oathState.locked) {
|
||||
return ListView(
|
||||
children: [
|
||||
|
@ -174,7 +174,7 @@ class ManagePasswordDialog extends ConsumerWidget {
|
||||
});
|
||||
|
||||
return ref.watch(oathStateProvider(device.path)).maybeWhen(
|
||||
success: (state) => ResponsiveDialog(
|
||||
data: (state) => ResponsiveDialog(
|
||||
title: const Text('Manage password'),
|
||||
child: _ManagePasswordForm(
|
||||
device.path,
|
||||
|
@ -7,7 +7,7 @@ authors = ["Dain Nilsson <dain@yubico.com>"]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
yubikey-manager = { git = "https://github.com/Yubico/yubikey-manager.git", rev = "22a72b2" }
|
||||
fido2 = { git = "https://github.com/Yubico/python-fido2.git", rev = "53175db" }
|
||||
fido2 = { git = "https://github.com/Yubico/python-fido2.git", rev = "fd30409" }
|
||||
mss = "^6.1.0"
|
||||
zxing-cpp = "^1.2.0"
|
||||
Pillow = "^8|^9"
|
||||
|
@ -85,6 +85,7 @@ def _handle_incoming(event, recv, error, cmd_queue):
|
||||
except RpcException as e:
|
||||
error(e.status, e.message, e.body)
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception")
|
||||
error("exception", f"{e!r}")
|
||||
event.set()
|
||||
cmd_queue.put(None)
|
||||
@ -126,6 +127,7 @@ def process(
|
||||
except RpcException as e:
|
||||
error(e.status, e.message, e.body)
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception")
|
||||
error("exception", f"{e!r}")
|
||||
cmd_queue.task_done()
|
||||
|
||||
|
@ -83,6 +83,11 @@ class TimeoutException(RpcException):
|
||||
super().__init__("timeout", "Command timed out waiting for user action")
|
||||
|
||||
|
||||
class AuthRequiredException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__("auth-required", "Authentication is required")
|
||||
|
||||
|
||||
class ChildResetException(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
@ -26,15 +26,18 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, action, child, RpcException, TimeoutException
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import (
|
||||
Ctap2,
|
||||
ClientPin,
|
||||
CredentialManagement,
|
||||
FPBioEnrollment,
|
||||
CaptureError,
|
||||
from .base import (
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
RpcException,
|
||||
TimeoutException,
|
||||
AuthRequiredException,
|
||||
)
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import Ctap2, ClientPin
|
||||
from fido2.ctap2.credman import CredentialManagement
|
||||
from fido2.ctap2.bio import BioEnrollment, FPBioEnrollment, CaptureError
|
||||
from fido2.pcsc import CtapPcscDevice
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from ykman.hid import list_ctap_devices as list_ctap
|
||||
@ -61,23 +64,32 @@ def _ctap_id(ctap):
|
||||
return (ctap.info.aaguid, ctap.info.firmware_version)
|
||||
|
||||
|
||||
def _handle_pin_error(e, client_pin):
|
||||
if e.code in (
|
||||
CtapError.ERR.PIN_INVALID,
|
||||
CtapError.ERR.PIN_BLOCKED,
|
||||
CtapError.ERR.PIN_AUTH_BLOCKED,
|
||||
):
|
||||
pin_retries, _ = client_pin.get_pin_retries()
|
||||
raise PinValidationException(
|
||||
pin_retries, e.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
class Ctap2Node(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self.ctap = Ctap2(connection)
|
||||
self._info = self.ctap.info
|
||||
self.client_pin = ClientPin(self.ctap)
|
||||
self._pin = None
|
||||
self._auth_blocked = False
|
||||
|
||||
def get_data(self):
|
||||
self._info = self.ctap.get_info()
|
||||
logger.debug(f"Info: {self._info}")
|
||||
data = dict(
|
||||
info=asdict(self._info), locked=False, auth_blocked=self._auth_blocked
|
||||
)
|
||||
data = dict(info=asdict(self._info), auth_blocked=self._auth_blocked)
|
||||
if self._info.options.get("clientPin"):
|
||||
data["locked"] = self._pin is None
|
||||
pin_retries, power_cycle = self.client_pin.get_pin_retries()
|
||||
data.update(
|
||||
pin_retries=pin_retries,
|
||||
@ -85,6 +97,7 @@ class Ctap2Node(RpcNode):
|
||||
)
|
||||
if self._info.options.get("bioEnroll"):
|
||||
uv_retries = self.client_pin.get_uv_retries()
|
||||
# For compatibility with python-fido2 < 1.0
|
||||
if isinstance(uv_retries, tuple):
|
||||
uv_retries = uv_retries[0]
|
||||
data.update(uv_retries=uv_retries)
|
||||
@ -155,22 +168,9 @@ class Ctap2Node(RpcNode):
|
||||
raise ValueError("Re-inserted YubiKey does not match initial device")
|
||||
self.ctap.reset(event)
|
||||
self._info = self.ctap.get_info()
|
||||
self._pin = None
|
||||
self._auth_blocked = False
|
||||
return dict()
|
||||
|
||||
def _handle_pin_error(self, e):
|
||||
if e.code in (
|
||||
CtapError.ERR.PIN_INVALID,
|
||||
CtapError.ERR.PIN_BLOCKED,
|
||||
CtapError.ERR.PIN_AUTH_BLOCKED,
|
||||
):
|
||||
pin_retries, _ = self.client_pin.get_pin_retries()
|
||||
raise PinValidationException(
|
||||
pin_retries, e.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
)
|
||||
raise e
|
||||
|
||||
@action(condition=lambda self: self._info.options["clientPin"])
|
||||
def verify_pin(self, params, event, signal):
|
||||
pin = params.pop("pin")
|
||||
@ -178,10 +178,9 @@ class Ctap2Node(RpcNode):
|
||||
self.client_pin.get_pin_token(
|
||||
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
|
||||
)
|
||||
self._pin = pin
|
||||
return dict()
|
||||
except CtapError as e:
|
||||
return self._handle_pin_error(e)
|
||||
return _handle_pin_error(e, self.client_pin)
|
||||
|
||||
@action
|
||||
def set_pin(self, params, event, signal):
|
||||
@ -196,22 +195,17 @@ class Ctap2Node(RpcNode):
|
||||
self.client_pin.set_pin(
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
self._pin = None
|
||||
return dict()
|
||||
except CtapError as e:
|
||||
return self._handle_pin_error(e)
|
||||
return _handle_pin_error(e, self.client_pin)
|
||||
|
||||
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
|
||||
@child(condition=lambda self: BioEnrollment.is_supported(self._info))
|
||||
def fingerprints(self):
|
||||
token = self.client_pin.get_pin_token(
|
||||
self._pin, ClientPin.PERMISSION.BIO_ENROLL
|
||||
)
|
||||
bio = FPBioEnrollment(self.ctap, self.client_pin.protocol, token)
|
||||
return FingerprintsNode(bio)
|
||||
return FingerprintsNode(self.client_pin)
|
||||
|
||||
# TODO: Use CredentialManagement.is_supported when released
|
||||
@child(condition=lambda self: self._pin)
|
||||
@child(condition=lambda self: CredentialManagement.is_supported(self._info))
|
||||
def credentials(self):
|
||||
return CredentialsRpsNode(self.client_pin)
|
||||
token = self.client_pin.get_pin_token(
|
||||
self._pin, ClientPin.PERMISSION.CREDENTIAL_MGMT
|
||||
)
|
||||
@ -220,10 +214,29 @@ class Ctap2Node(RpcNode):
|
||||
|
||||
|
||||
class CredentialsRpsNode(RpcNode):
|
||||
def __init__(self, credman):
|
||||
def __init__(self, client_pin):
|
||||
super().__init__()
|
||||
self.credman = credman
|
||||
self.refresh()
|
||||
self.client_pin = client_pin
|
||||
self.credman = None
|
||||
self._rps = {}
|
||||
|
||||
def get_data(self):
|
||||
return dict(locked=self.credman is None)
|
||||
|
||||
@action
|
||||
def unlock(self, params, event, signal):
|
||||
pin = params.pop("pin")
|
||||
try:
|
||||
token = self.client_pin.get_pin_token(
|
||||
pin, ClientPin.PERMISSION.CREDENTIAL_MGMT
|
||||
)
|
||||
self.credman = CredentialManagement(
|
||||
self.client_pin.ctap, self.client_pin.protocol, token
|
||||
)
|
||||
self.refresh()
|
||||
return dict()
|
||||
except CtapError as e:
|
||||
return _handle_pin_error(e, self.client_pin)
|
||||
|
||||
def refresh(self):
|
||||
data = self.credman.get_metadata()
|
||||
@ -296,13 +309,34 @@ class CredentialNode(RpcNode):
|
||||
|
||||
|
||||
class FingerprintsNode(RpcNode):
|
||||
def __init__(self, bio):
|
||||
def __init__(self, client_pin):
|
||||
super().__init__()
|
||||
self.bio = bio
|
||||
self.refresh()
|
||||
self.client_pin = client_pin
|
||||
self.bio = None
|
||||
self._templates = {}
|
||||
|
||||
def get_data(self):
|
||||
return dict(locked=self.bio is None)
|
||||
|
||||
@action
|
||||
def unlock(self, params, event, signal):
|
||||
pin = params.pop("pin")
|
||||
try:
|
||||
token = self.client_pin.get_pin_token(pin, ClientPin.PERMISSION.BIO_ENROLL)
|
||||
self.bio = FPBioEnrollment(
|
||||
self.client_pin.ctap, self.client_pin.protocol, token
|
||||
)
|
||||
self.refresh()
|
||||
return dict()
|
||||
except CtapError as e:
|
||||
return _handle_pin_error(e, self.client_pin)
|
||||
|
||||
def refresh(self):
|
||||
self._templates = self.bio.enumerate_enrollments()
|
||||
self._templates = {
|
||||
# Treat empty strings as None
|
||||
k: v if v else None
|
||||
for k, v in self.bio.enumerate_enrollments().items()
|
||||
}
|
||||
|
||||
def list_children(self):
|
||||
return {
|
||||
@ -320,6 +354,8 @@ class FingerprintsNode(RpcNode):
|
||||
|
||||
@action
|
||||
def add(self, params, event, signal):
|
||||
if self.bio is None:
|
||||
raise AuthRequiredException()
|
||||
name = params.get("name", None)
|
||||
enroller = self.bio.enroll()
|
||||
template_id = None
|
||||
|
@ -32,7 +32,7 @@ from .base import (
|
||||
child,
|
||||
ChildResetException,
|
||||
TimeoutException,
|
||||
RpcException,
|
||||
AuthRequiredException,
|
||||
encode_bytes,
|
||||
decode_bytes,
|
||||
)
|
||||
@ -50,11 +50,6 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthRequiredException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__("auth-required", "Authentication is required")
|
||||
|
||||
|
||||
@unique
|
||||
class KEYSTORE(str, Enum):
|
||||
UNKNOWN = "unknown"
|
||||
|
Loading…
Reference in New Issue
Block a user