mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Add FIDO PIN management.
This commit is contained in:
parent
a864787329
commit
b71d17386a
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:yubico_authenticator/desktop/models.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../fido/models.dart';
|
import '../../fido/models.dart';
|
||||||
@ -49,19 +50,30 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> reset() {
|
Future<void> reset() async {
|
||||||
// TODO: implement reset
|
// TODO: implement reset
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setPin(String newPin, {String? oldPin}) {
|
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
||||||
// TODO: implement setPin
|
try {
|
||||||
throw UnimplementedError();
|
await _session.command('set_pin', params: {
|
||||||
|
'pin': oldPin,
|
||||||
|
'new_pin': newPin,
|
||||||
|
});
|
||||||
|
return PinResult.success();
|
||||||
|
} on RpcError catch (e) {
|
||||||
|
if (e.status == 'pin-validation') {
|
||||||
|
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// TODO: Update state
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> unlock(String pin) {
|
Future<PinResult> unlock(String pin) {
|
||||||
// TODO: implement unlock
|
// TODO: implement unlock
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,18 @@ class FidoState with _$FidoState {
|
|||||||
_$FidoStateFromJson(json);
|
_$FidoStateFromJson(json);
|
||||||
|
|
||||||
bool get hasPin => info['options']['clientPin'] == true;
|
bool get hasPin => info['options']['clientPin'] == true;
|
||||||
|
|
||||||
|
int get minPinLength => info['min_pin_length'] as int;
|
||||||
|
|
||||||
|
bool get credMgmt =>
|
||||||
|
info['options']['credMgmt'] == true ||
|
||||||
|
info['options']['credentialMgmtPreview'] == true;
|
||||||
|
|
||||||
|
bool? get bioEnroll => info['options']['bioEnroll'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class PinResult with _$PinResult {
|
||||||
|
factory PinResult.success() = _Success;
|
||||||
|
factory PinResult.failed(int retries, bool authBlocked) = _Failure;
|
||||||
}
|
}
|
||||||
|
@ -178,3 +178,324 @@ abstract class _FidoState extends FidoState {
|
|||||||
_$FidoStateCopyWith<_FidoState> get copyWith =>
|
_$FidoStateCopyWith<_FidoState> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PinResultTearOff {
|
||||||
|
const _$PinResultTearOff();
|
||||||
|
|
||||||
|
_Success success() {
|
||||||
|
return _Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
_Failure failed(int retries, bool authBlocked) {
|
||||||
|
return _Failure(
|
||||||
|
retries,
|
||||||
|
authBlocked,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
const $PinResult = _$PinResultTearOff();
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$PinResult {
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() success,
|
||||||
|
required TResult Function(int retries, bool authBlocked) failed,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(_Success value) success,
|
||||||
|
required TResult Function(_Failure value) failed,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PinResultCopyWith<$Res> {
|
||||||
|
factory $PinResultCopyWith(PinResult value, $Res Function(PinResult) then) =
|
||||||
|
_$PinResultCopyWithImpl<$Res>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PinResultCopyWithImpl<$Res> implements $PinResultCopyWith<$Res> {
|
||||||
|
_$PinResultCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
final PinResult _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function(PinResult) _then;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$SuccessCopyWith<$Res> {
|
||||||
|
factory _$SuccessCopyWith(_Success value, $Res Function(_Success) then) =
|
||||||
|
__$SuccessCopyWithImpl<$Res>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$SuccessCopyWithImpl<$Res> extends _$PinResultCopyWithImpl<$Res>
|
||||||
|
implements _$SuccessCopyWith<$Res> {
|
||||||
|
__$SuccessCopyWithImpl(_Success _value, $Res Function(_Success) _then)
|
||||||
|
: super(_value, (v) => _then(v as _Success));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_Success get _value => super._value as _Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_Success implements _Success {
|
||||||
|
_$_Success();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PinResult.success()';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType && other is _Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => runtimeType.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() success,
|
||||||
|
required TResult Function(int retries, bool authBlocked) failed,
|
||||||
|
}) {
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
}) {
|
||||||
|
return success?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (success != null) {
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(_Success value) success,
|
||||||
|
required TResult Function(_Failure value) failed,
|
||||||
|
}) {
|
||||||
|
return success(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
}) {
|
||||||
|
return success?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (success != null) {
|
||||||
|
return success(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Success implements PinResult {
|
||||||
|
factory _Success() = _$_Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$FailureCopyWith<$Res> {
|
||||||
|
factory _$FailureCopyWith(_Failure value, $Res Function(_Failure) then) =
|
||||||
|
__$FailureCopyWithImpl<$Res>;
|
||||||
|
$Res call({int retries, bool authBlocked});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$FailureCopyWithImpl<$Res> extends _$PinResultCopyWithImpl<$Res>
|
||||||
|
implements _$FailureCopyWith<$Res> {
|
||||||
|
__$FailureCopyWithImpl(_Failure _value, $Res Function(_Failure) _then)
|
||||||
|
: super(_value, (v) => _then(v as _Failure));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_Failure get _value => super._value as _Failure;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? retries = freezed,
|
||||||
|
Object? authBlocked = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_Failure(
|
||||||
|
retries == freezed
|
||||||
|
? _value.retries
|
||||||
|
: retries // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
authBlocked == freezed
|
||||||
|
? _value.authBlocked
|
||||||
|
: authBlocked // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_Failure implements _Failure {
|
||||||
|
_$_Failure(this.retries, this.authBlocked);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int retries;
|
||||||
|
@override
|
||||||
|
final bool authBlocked;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PinResult.failed(retries: $retries, authBlocked: $authBlocked)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _Failure &&
|
||||||
|
const DeepCollectionEquality().equals(other.retries, retries) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.authBlocked, authBlocked));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(retries),
|
||||||
|
const DeepCollectionEquality().hash(authBlocked));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
_$FailureCopyWith<_Failure> get copyWith =>
|
||||||
|
__$FailureCopyWithImpl<_Failure>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() success,
|
||||||
|
required TResult Function(int retries, bool authBlocked) failed,
|
||||||
|
}) {
|
||||||
|
return failed(retries, authBlocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
}) {
|
||||||
|
return failed?.call(retries, authBlocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? success,
|
||||||
|
TResult Function(int retries, bool authBlocked)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (failed != null) {
|
||||||
|
return failed(retries, authBlocked);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(_Success value) success,
|
||||||
|
required TResult Function(_Failure value) failed,
|
||||||
|
}) {
|
||||||
|
return failed(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
}) {
|
||||||
|
return failed?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(_Success value)? success,
|
||||||
|
TResult Function(_Failure value)? failed,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (failed != null) {
|
||||||
|
return failed(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Failure implements PinResult {
|
||||||
|
factory _Failure(int retries, bool authBlocked) = _$_Failure;
|
||||||
|
|
||||||
|
int get retries;
|
||||||
|
bool get authBlocked;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$FailureCopyWith<_Failure> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
@ -11,6 +11,6 @@ final fidoStateProvider = StateNotifierProvider.autoDispose
|
|||||||
|
|
||||||
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||||
Future<void> reset();
|
Future<void> reset();
|
||||||
Future<void> unlock(String pin);
|
Future<PinResult> unlock(String pin);
|
||||||
Future<void> setPin(String newPin, {String? oldPin});
|
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:yubico_authenticator/desktop/state.dart';
|
import 'package:yubico_authenticator/desktop/state.dart';
|
||||||
|
import 'package:yubico_authenticator/fido/views/pin_dialog.dart';
|
||||||
|
import 'package:yubico_authenticator/management/models.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/views/app_failure_screen.dart';
|
import '../../app/views/app_failure_screen.dart';
|
||||||
@ -18,6 +20,14 @@ class FidoScreen extends ConsumerWidget {
|
|||||||
ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||||
none: () => const AppLoadingScreen(),
|
none: () => const AppLoadingScreen(),
|
||||||
failure: (reason) {
|
failure: (reason) {
|
||||||
|
final fido2 = deviceData.info
|
||||||
|
.supportedCapabilities[deviceData.node.transport]! &
|
||||||
|
Capability.fido2.value !=
|
||||||
|
0;
|
||||||
|
if (!fido2) {
|
||||||
|
return const AppFailureScreen(
|
||||||
|
'WebAuthn is supported by this device, but there are no management options available.');
|
||||||
|
}
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
if (!ref
|
if (!ref
|
||||||
.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||||
@ -25,11 +35,54 @@ class FidoScreen extends ConsumerWidget {
|
|||||||
'WebAuthn management requires elevated privileges.\nRestart this app as administrator.');
|
'WebAuthn management requires elevated privileges.\nRestart this app as administrator.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (deviceData.info
|
||||||
|
.supportedCapabilities[deviceData.node.transport]! &
|
||||||
|
Capability.fido2.value ==
|
||||||
|
0) {}
|
||||||
return AppFailureScreen(reason);
|
return AppFailureScreen(reason);
|
||||||
},
|
},
|
||||||
success: (state) => ListView(
|
success: (state) => ListView(
|
||||||
children: [
|
children: [
|
||||||
Text('${state.info}'),
|
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'),
|
||||||
|
),
|
||||||
|
const ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: Icon(Icons.delete_forever),
|
||||||
|
),
|
||||||
|
title: Text('Factory reset'),
|
||||||
|
subtitle: Text('Delete all data and remove PIN'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
147
lib/fido/views/pin_dialog.dart
Executable file
147
lib/fido/views/pin_dialog.dart
Executable file
@ -0,0 +1,147 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../app/views/responsive_dialog.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
|
||||||
|
class FidoPinDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final FidoState state;
|
||||||
|
const FidoPinDialog(this.devicePath, this.state, {Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _FidoPinDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||||
|
String _currentPin = '';
|
||||||
|
String _newPin = '';
|
||||||
|
String _confirmPin = '';
|
||||||
|
String? _currentPinError;
|
||||||
|
String? _newPinError;
|
||||||
|
|
||||||
|
@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 minPinLength = widget.state.minPinLength;
|
||||||
|
final hasPin = widget.state.hasPin;
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(hasPin ? 'Change PIN' : 'Set PIN'),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (hasPin) ...[
|
||||||
|
Text(
|
||||||
|
'Current PIN',
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _currentPin,
|
||||||
|
autofocus: true,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'Current PIN',
|
||||||
|
errorText: _currentPinError,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_currentPin = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
'New PIN',
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _newPin,
|
||||||
|
autofocus: !hasPin,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'New password',
|
||||||
|
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||||
|
errorText: _newPinError,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newPin = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _confirmPin,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'Confirm password',
|
||||||
|
enabled: _newPin.isNotEmpty,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_confirmPin = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Save'),
|
||||||
|
onPressed: _newPin.isNotEmpty &&
|
||||||
|
_newPin == _confirmPin &&
|
||||||
|
(!hasPin || _currentPin.isNotEmpty)
|
||||||
|
? () async {
|
||||||
|
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
|
||||||
|
if (_newPin.length < minPinLength) {
|
||||||
|
setState(() {
|
||||||
|
_newPinError =
|
||||||
|
'New PIN must be at least $minPinLength characters';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await ref
|
||||||
|
.read(fidoStateProvider(widget.devicePath).notifier)
|
||||||
|
.setPin(_newPin, oldPin: oldPin);
|
||||||
|
result.when(success: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('PIN set'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, failed: (retries, authBlocked) {
|
||||||
|
setState(() {
|
||||||
|
if (authBlocked) {
|
||||||
|
_currentPinError =
|
||||||
|
'PIN has been blocked until the YubiKey is removed and reinserted';
|
||||||
|
} else {
|
||||||
|
_currentPinError =
|
||||||
|
'Wrong PIN ($retries tries remaining)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,8 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
from .base import RpcNode, action, child
|
from .base import RpcNode, action, child, RpcException
|
||||||
|
from fido2.ctap import CtapError
|
||||||
from fido2.ctap2 import (
|
from fido2.ctap2 import (
|
||||||
Ctap2,
|
Ctap2,
|
||||||
ClientPin,
|
ClientPin,
|
||||||
@ -40,6 +41,15 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PinValidationException(RpcException):
|
||||||
|
def __init__(self, retries, auth_blocked):
|
||||||
|
super().__init__(
|
||||||
|
"pin-validation",
|
||||||
|
"Authentication is required",
|
||||||
|
dict(retries=retries, auth_blocked=auth_blocked),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Ctap2Node(RpcNode):
|
class Ctap2Node(RpcNode):
|
||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -47,11 +57,14 @@ class Ctap2Node(RpcNode):
|
|||||||
self._info = self.ctap.info
|
self._info = self.ctap.info
|
||||||
self.client_pin = ClientPin(self.ctap)
|
self.client_pin = ClientPin(self.ctap)
|
||||||
self._pin = None
|
self._pin = None
|
||||||
|
self._auth_blocked = False
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
self._info = self.ctap.get_info()
|
self._info = self.ctap.get_info()
|
||||||
logger.debug(f"Info: {self._info}")
|
logger.debug(f"Info: {self._info}")
|
||||||
data = dict(info=asdict(self._info), locked=False)
|
data = dict(
|
||||||
|
info=asdict(self._info), locked=False, auth_blocked=self._auth_blocked
|
||||||
|
)
|
||||||
if self._info.options.get("clientPin"):
|
if self._info.options.get("clientPin"):
|
||||||
data["locked"] = self._pin is None
|
data["locked"] = self._pin is None
|
||||||
pin_retries, power_cycle = self.client_pin.get_pin_retries()
|
pin_retries, power_cycle = self.client_pin.get_pin_retries()
|
||||||
@ -70,20 +83,37 @@ class Ctap2Node(RpcNode):
|
|||||||
def reset(self, params, event, signal):
|
def reset(self, params, event, signal):
|
||||||
self.ctap.reset(event)
|
self.ctap.reset(event)
|
||||||
self._pin = None
|
self._pin = None
|
||||||
|
self._auth_blocked = False
|
||||||
return dict()
|
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"])
|
@action(condition=lambda self: self._info.options["clientPin"])
|
||||||
def verify_pin(self, params, event, signal):
|
def verify_pin(self, params, event, signal):
|
||||||
pin = params.pop("pin")
|
pin = params.pop("pin")
|
||||||
|
try:
|
||||||
self.client_pin.get_pin_token(
|
self.client_pin.get_pin_token(
|
||||||
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
|
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
|
||||||
)
|
)
|
||||||
self._pin = pin
|
self._pin = pin
|
||||||
return dict()
|
return dict()
|
||||||
|
except CtapError as e:
|
||||||
|
return self._handle_pin_error(e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_pin(self, params, event, signal):
|
def set_pin(self, params, event, signal):
|
||||||
has_pin = self.ctap.get_info().options["clientPin"]
|
has_pin = self.ctap.get_info().options["clientPin"]
|
||||||
|
try:
|
||||||
if has_pin:
|
if has_pin:
|
||||||
self.client_pin.change_pin(
|
self.client_pin.change_pin(
|
||||||
params.pop("pin"),
|
params.pop("pin"),
|
||||||
@ -95,6 +125,8 @@ class Ctap2Node(RpcNode):
|
|||||||
)
|
)
|
||||||
self._pin = None
|
self._pin = None
|
||||||
return dict()
|
return dict()
|
||||||
|
except CtapError as e:
|
||||||
|
return self._handle_pin_error(e)
|
||||||
|
|
||||||
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
|
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
|
||||||
def fingerprints(self):
|
def fingerprints(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user