mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Add basic management screen.
This commit is contained in:
parent
0cf575d7c3
commit
d42eb84d04
@ -10,6 +10,7 @@ import 'device_info_screen.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../../oath/views/oath_screen.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
|
||||
class MainPage extends ConsumerWidget {
|
||||
const MainPage({Key? key}) : super(key: key);
|
||||
@ -22,6 +23,8 @@ class MainPage extends ConsumerWidget {
|
||||
switch (subPage) {
|
||||
case SubPage.oath:
|
||||
return OathScreen(device);
|
||||
case SubPage.management:
|
||||
return ManagementScreen(device);
|
||||
default:
|
||||
return DeviceInfoScreen(device);
|
||||
}
|
||||
|
@ -6,9 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:yubico_authenticator/management/state.dart';
|
||||
|
||||
import '../oath/state.dart';
|
||||
import '../app/state.dart';
|
||||
import 'management/state.dart';
|
||||
import 'oath/state.dart';
|
||||
import 'rpc.dart';
|
||||
import 'devices.dart';
|
||||
@ -86,5 +88,6 @@ Future<List<Override>> initializeAndGetOverrides(
|
||||
credentialListProvider
|
||||
.overrideWithProvider(desktopOathCredentialListProvider),
|
||||
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
||||
managementStateProvider.overrideWithProvider(desktopManagementState),
|
||||
];
|
||||
}
|
||||
|
85
lib/desktop/management/state.dart
Executable file
85
lib/desktop/management/state.dart
Executable file
@ -0,0 +1,85 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/management/models.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../management/state.dart';
|
||||
import '../rpc.dart';
|
||||
import '../state.dart';
|
||||
|
||||
final _log = Logger('desktop.management.state');
|
||||
|
||||
final _sessionProvider =
|
||||
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final protocol = ref.watch(currentDeviceProvider)!.when(
|
||||
usbYubiKey: (path, name, pid, info) {
|
||||
final interfaces = UsbInterfaces.forCapabilites(
|
||||
info.config.enabledCapabilities[Transport.usb] ?? 0);
|
||||
return [UsbInterface.ccid, UsbInterface.otp, UsbInterface.fido]
|
||||
.firstWhere((iface) => iface.value & interfaces != 0);
|
||||
},
|
||||
nfcReader: (_, __) => UsbInterface.ccid,
|
||||
);
|
||||
return RpcNodeSession(
|
||||
ref.watch(rpcProvider), devicePath, [protocol.name, 'management']);
|
||||
},
|
||||
);
|
||||
|
||||
final desktopManagementState = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, DeviceInfo?, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopManagementStateNotifier(session);
|
||||
session.setErrorHandler('state-reset', (_) async {
|
||||
ref.refresh(_sessionProvider(devicePath));
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unserErrorHandler('state-reset');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
);
|
||||
|
||||
class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
_DesktopManagementStateNotifier(this._session) : super();
|
||||
|
||||
void refresh() async {
|
||||
var result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
if (mounted) {
|
||||
state = DeviceInfo.fromJson(result['data']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setMode(int mode,
|
||||
{int challengeResponseTimeout = 0, int autoEjectTimeout = 0}) async {
|
||||
await _session.command('set_mode', params: {
|
||||
'mode': mode,
|
||||
'challenge_response_timeout': challengeResponseTimeout,
|
||||
'auto_eject_timeout': autoEjectTimeout,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeConfig(DeviceConfig config,
|
||||
{String currentLockCode = '',
|
||||
String newLockCode = '',
|
||||
bool reboot = false}) async {
|
||||
if (reboot) {
|
||||
state = null;
|
||||
}
|
||||
await _session.command('configure', params: {
|
||||
...config.toJson(),
|
||||
'cur_lock_code': currentLockCode,
|
||||
'new_lock_code': newLockCode,
|
||||
'reboot': reboot,
|
||||
});
|
||||
}
|
||||
}
|
@ -26,6 +26,82 @@ enum FormFactor {
|
||||
usbCBio,
|
||||
}
|
||||
|
||||
enum UsbInterface { otp, fido, ccid }
|
||||
|
||||
extension UsbInterfaces on UsbInterface {
|
||||
int get value {
|
||||
switch (this) {
|
||||
case UsbInterface.otp:
|
||||
return 0x01;
|
||||
case UsbInterface.fido:
|
||||
return 0x02;
|
||||
case UsbInterface.ccid:
|
||||
return 0x04;
|
||||
}
|
||||
}
|
||||
|
||||
static int forCapabilites(int capabilities) {
|
||||
var interfaces = 0;
|
||||
if (capabilities & Capability.otp.value != 0) {
|
||||
interfaces |= UsbInterface.otp.value;
|
||||
}
|
||||
if (capabilities & (Capability.u2f.value | Capability.fido2.value) != 0) {
|
||||
interfaces |= UsbInterface.fido.value;
|
||||
}
|
||||
if (capabilities &
|
||||
(Capability.openpgp.value |
|
||||
Capability.piv.value |
|
||||
Capability.oath.value |
|
||||
Capability.hsmauth.value) !=
|
||||
0) {
|
||||
interfaces |= UsbInterface.ccid.value;
|
||||
}
|
||||
return interfaces;
|
||||
}
|
||||
}
|
||||
|
||||
enum Capability { otp, u2f, openpgp, piv, oath, hsmauth, fido2 }
|
||||
|
||||
extension CapabilityExtension on Capability {
|
||||
int get value {
|
||||
switch (this) {
|
||||
case Capability.otp:
|
||||
return 0x001;
|
||||
case Capability.u2f:
|
||||
return 0x002;
|
||||
case Capability.openpgp:
|
||||
return 0x008;
|
||||
case Capability.piv:
|
||||
return 0x010;
|
||||
case Capability.oath:
|
||||
return 0x020;
|
||||
case Capability.hsmauth:
|
||||
return 0x100;
|
||||
case Capability.fido2:
|
||||
return 0x200;
|
||||
}
|
||||
}
|
||||
|
||||
String get name {
|
||||
switch (this) {
|
||||
case Capability.otp:
|
||||
return 'OTP';
|
||||
case Capability.u2f:
|
||||
return 'FIDO U2F';
|
||||
case Capability.openpgp:
|
||||
return 'OpenPGP';
|
||||
case Capability.piv:
|
||||
return 'PIV';
|
||||
case Capability.oath:
|
||||
return 'OATH';
|
||||
case Capability.hsmauth:
|
||||
return 'YubiHSM Auth';
|
||||
case Capability.fido2:
|
||||
return 'FIDO2';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DeviceConfig with _$DeviceConfig {
|
||||
factory DeviceConfig(
|
||||
|
21
lib/management/state.dart
Executable file
21
lib/management/state.dart
Executable file
@ -0,0 +1,21 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/management/models.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
|
||||
final managementStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, DeviceInfo?, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class ManagementStateNotifier extends StateNotifier<DeviceInfo?> {
|
||||
ManagementStateNotifier() : super(null);
|
||||
|
||||
Future<void> writeConfig(DeviceConfig config,
|
||||
{String currentLockCode = '',
|
||||
String newLockCode = '',
|
||||
bool reboot = false});
|
||||
|
||||
Future<void> setMode(int mode,
|
||||
{int challengeResponseTimeout = 0, int autoEjectTimeout = 0});
|
||||
}
|
180
lib/management/views/management_screen.dart
Executable file
180
lib/management/views/management_screen.dart
Executable file
@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
|
||||
final _mapEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
class _CapabilityForm extends StatelessWidget {
|
||||
final int capabilities;
|
||||
final int enabled;
|
||||
final Function(int) onChanged;
|
||||
const _CapabilityForm(
|
||||
{required this.capabilities,
|
||||
required this.enabled,
|
||||
required this.onChanged,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 6.0,
|
||||
runSpacing: 6.0,
|
||||
children: Capability.values
|
||||
.where((c) => capabilities & c.value != 0)
|
||||
.map((c) => FilterChip(
|
||||
showCheckmark: true,
|
||||
selected: enabled & c.value != 0,
|
||||
label: Text(c.name),
|
||||
onSelected: (_) {
|
||||
onChanged(enabled ^ c.value);
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CapabilitiesForm extends StatefulWidget {
|
||||
final DeviceInfo info;
|
||||
final Function(Map<Transport, int>) onSubmit;
|
||||
|
||||
const _CapabilitiesForm(this.info, {required this.onSubmit, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CapabilitiesFormState();
|
||||
}
|
||||
|
||||
class _CapabilitiesFormState extends State<_CapabilitiesForm> {
|
||||
late Map<Transport, int> _enabled;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Make sure to copy enabledCapabilites, not mutate the original.
|
||||
_enabled = {...widget.info.config.enabledCapabilities};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final usbCapabilities =
|
||||
widget.info.supportedCapabilities[Transport.usb] ?? 0;
|
||||
final nfcCapabilities =
|
||||
widget.info.supportedCapabilities[Transport.nfc] ?? 0;
|
||||
|
||||
final changed =
|
||||
!_mapEquals(widget.info.config.enabledCapabilities, _enabled);
|
||||
final valid = changed && (_enabled[Transport.usb] ?? 0) > 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (usbCapabilities != 0)
|
||||
const ListTile(
|
||||
leading: Icon(Icons.usb),
|
||||
title: Text('USB applications'),
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: usbCapabilities,
|
||||
enabled: _enabled[Transport.usb] ?? 0,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
_enabled[Transport.usb] = enabled;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (nfcCapabilities != 0)
|
||||
const ListTile(
|
||||
leading: Icon(Icons.wifi),
|
||||
title: Text('NFC applications'),
|
||||
),
|
||||
_CapabilityForm(
|
||||
capabilities: nfcCapabilities,
|
||||
enabled: _enabled[Transport.nfc] ?? 0,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
_enabled[Transport.nfc] = enabled;
|
||||
});
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: valid
|
||||
? () {
|
||||
widget.onSubmit(_enabled);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Apply changes'),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ManagementScreen extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
const ManagementScreen(this.deviceData, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(managementStateProvider(deviceData.node.path));
|
||||
|
||||
if (state == null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_CapabilitiesForm(state, onSubmit: (enabled) async {
|
||||
final bool reboot;
|
||||
if (deviceData.node is UsbYubiKeyNode) {
|
||||
final oldInterfaces = UsbInterfaces.forCapabilites(
|
||||
state.config.enabledCapabilities[Transport.usb] ?? 0);
|
||||
final newInterfaces =
|
||||
UsbInterfaces.forCapabilites(enabled[Transport.usb] ?? 0);
|
||||
reboot = oldInterfaces != newInterfaces;
|
||||
} else {
|
||||
reboot = false;
|
||||
}
|
||||
|
||||
Function()? close;
|
||||
try {
|
||||
if (reboot) {
|
||||
close = ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text('Updating configuration...'),
|
||||
duration: Duration(seconds: 8),
|
||||
))
|
||||
.close;
|
||||
}
|
||||
await ref
|
||||
.read(managementStateProvider(deviceData.node.path).notifier)
|
||||
.writeConfig(
|
||||
state.config.copyWith(enabledCapabilities: enabled),
|
||||
reboot: reboot,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Configuration updated'),
|
||||
duration: Duration(seconds: 2),
|
||||
));
|
||||
} finally {
|
||||
close?.call();
|
||||
}
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -27,14 +27,23 @@
|
||||
|
||||
|
||||
from .base import RpcNode, action
|
||||
from yubikit.core import require_version, NotSupportedError
|
||||
from yubikit.management import ManagementSession, DeviceConfig
|
||||
from yubikit.core import require_version, NotSupportedError, TRANSPORT
|
||||
from yubikit.core.smartcard import SmartCardConnection
|
||||
from yubikit.core.otp import OtpConnection
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from yubikit.management import ManagementSession, DeviceConfig, USB_INTERFACE
|
||||
from ykman.device import connect_to_device
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManagementNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self._connection_type = type(connection)
|
||||
self.session = ManagementSession(connection)
|
||||
|
||||
def get_data(self):
|
||||
@ -49,6 +58,41 @@ class ManagementNode(RpcNode):
|
||||
actions.remove("configure")
|
||||
return actions
|
||||
|
||||
def _await_reboot(self, serial, usb_enabled):
|
||||
# TODO: Clean up once "support" is merged into ykman.
|
||||
iface = USB_INTERFACE.for_capabilities(usb_enabled)
|
||||
connection_types = []
|
||||
|
||||
# Prefer to use the "same" connection type as before
|
||||
if iface.supports_connection(self._connection_type):
|
||||
if issubclass(self._connection_type, SmartCardConnection):
|
||||
connection_types = [SmartCardConnection]
|
||||
elif issubclass(self._connection_type, OtpConnection):
|
||||
connection_types = [OtpConnection]
|
||||
elif issubclass(self._connection_type, FidoConnection):
|
||||
connection_types = [FidoConnection]
|
||||
|
||||
# Allow any expected connection type
|
||||
if not connection_types:
|
||||
connection_types = [
|
||||
t
|
||||
for t in [SmartCardConnection, OtpConnection, FidoConnection]
|
||||
if iface.supports_connection(t)
|
||||
]
|
||||
|
||||
self.session.close()
|
||||
logger.debug("Waiting for device to re-appear...")
|
||||
for _ in range(10):
|
||||
sleep(0.2) # Always sleep initially
|
||||
try:
|
||||
conn = connect_to_device(serial, connection_types)[0]
|
||||
conn.close()
|
||||
break
|
||||
except ValueError:
|
||||
logger.debug("Not found, sleep...")
|
||||
else:
|
||||
logger.warning("Timed out waiting for device")
|
||||
|
||||
@action
|
||||
def configure(self, params, event, signal):
|
||||
reboot = params.pop("reboot", False)
|
||||
@ -60,7 +104,11 @@ class ManagementNode(RpcNode):
|
||||
params.pop("challenge_response_timeout", None),
|
||||
params.pop("device_flags", None),
|
||||
)
|
||||
serial = self.session.read_device_info().serial
|
||||
self.session.write_device_config(config, reboot, cur_lock_code, new_lock_code)
|
||||
if reboot:
|
||||
enabled = config.enabled_capabilities.get(TRANSPORT.USB)
|
||||
self._await_reboot(serial, enabled)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
|
Loading…
Reference in New Issue
Block a user