mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 02:01:36 +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:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/desktop/models.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../fido/models.dart';
|
||||
@ -49,19 +50,30 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reset() {
|
||||
Future<void> reset() async {
|
||||
// TODO: implement reset
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setPin(String newPin, {String? oldPin}) {
|
||||
// TODO: implement setPin
|
||||
throw UnimplementedError();
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
||||
try {
|
||||
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
|
||||
Future<void> unlock(String pin) {
|
||||
Future<PinResult> unlock(String pin) {
|
||||
// TODO: implement unlock
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -16,4 +16,18 @@ class FidoState with _$FidoState {
|
||||
_$FidoStateFromJson(json);
|
||||
|
||||
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 =>
|
||||
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> {
|
||||
Future<void> reset();
|
||||
Future<void> unlock(String pin);
|
||||
Future<void> setPin(String newPin, {String? oldPin});
|
||||
Future<PinResult> unlock(String pin);
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/views/app_failure_screen.dart';
|
||||
@ -18,6 +20,14 @@ class FidoScreen extends ConsumerWidget {
|
||||
ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||
none: () => const AppLoadingScreen(),
|
||||
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 (!ref
|
||||
.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
@ -25,11 +35,54 @@ class FidoScreen extends ConsumerWidget {
|
||||
'WebAuthn management requires elevated privileges.\nRestart this app as administrator.');
|
||||
}
|
||||
}
|
||||
if (deviceData.info
|
||||
.supportedCapabilities[deviceData.node.transport]! &
|
||||
Capability.fido2.value ==
|
||||
0) {}
|
||||
return AppFailureScreen(reason);
|
||||
},
|
||||
success: (state) => ListView(
|
||||
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.
|
||||
|
||||
|
||||
from .base import RpcNode, action, child
|
||||
from .base import RpcNode, action, child, RpcException
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import (
|
||||
Ctap2,
|
||||
ClientPin,
|
||||
@ -40,6 +41,15 @@ import logging
|
||||
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):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
@ -47,11 +57,14 @@ class Ctap2Node(RpcNode):
|
||||
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)
|
||||
data = dict(
|
||||
info=asdict(self._info), locked=False, 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()
|
||||
@ -70,31 +83,50 @@ class Ctap2Node(RpcNode):
|
||||
def reset(self, params, event, signal):
|
||||
self.ctap.reset(event)
|
||||
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")
|
||||
self.client_pin.get_pin_token(
|
||||
pin, ClientPin.PERMISSION.GET_ASSERTION, "ykman.example.com"
|
||||
)
|
||||
self._pin = pin
|
||||
return dict()
|
||||
try:
|
||||
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)
|
||||
|
||||
@action
|
||||
def set_pin(self, params, event, signal):
|
||||
has_pin = self.ctap.get_info().options["clientPin"]
|
||||
if has_pin:
|
||||
self.client_pin.change_pin(
|
||||
params.pop("pin"),
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
else:
|
||||
self.client_pin.set_pin(
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
self._pin = None
|
||||
return dict()
|
||||
try:
|
||||
if has_pin:
|
||||
self.client_pin.change_pin(
|
||||
params.pop("pin"),
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
else:
|
||||
self.client_pin.set_pin(
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
self._pin = None
|
||||
return dict()
|
||||
except CtapError as e:
|
||||
return self._handle_pin_error(e)
|
||||
|
||||
@child(condition=lambda self: "bioEnroll" in self._info.options and self._pin)
|
||||
def fingerprints(self):
|
||||
|
Loading…
Reference in New Issue
Block a user