mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +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 '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../oath/views/oath_screen.dart';
|
import '../../oath/views/oath_screen.dart';
|
||||||
|
import '../../management/views/management_screen.dart';
|
||||||
|
|
||||||
class MainPage extends ConsumerWidget {
|
class MainPage extends ConsumerWidget {
|
||||||
const MainPage({Key? key}) : super(key: key);
|
const MainPage({Key? key}) : super(key: key);
|
||||||
@ -22,6 +23,8 @@ class MainPage extends ConsumerWidget {
|
|||||||
switch (subPage) {
|
switch (subPage) {
|
||||||
case SubPage.oath:
|
case SubPage.oath:
|
||||||
return OathScreen(device);
|
return OathScreen(device);
|
||||||
|
case SubPage.management:
|
||||||
|
return ManagementScreen(device);
|
||||||
default:
|
default:
|
||||||
return DeviceInfoScreen(device);
|
return DeviceInfoScreen(device);
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:yubico_authenticator/management/state.dart';
|
||||||
|
|
||||||
import '../oath/state.dart';
|
import '../oath/state.dart';
|
||||||
import '../app/state.dart';
|
import '../app/state.dart';
|
||||||
|
import 'management/state.dart';
|
||||||
import 'oath/state.dart';
|
import 'oath/state.dart';
|
||||||
import 'rpc.dart';
|
import 'rpc.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
@ -86,5 +88,6 @@ Future<List<Override>> initializeAndGetOverrides(
|
|||||||
credentialListProvider
|
credentialListProvider
|
||||||
.overrideWithProvider(desktopOathCredentialListProvider),
|
.overrideWithProvider(desktopOathCredentialListProvider),
|
||||||
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
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,
|
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
|
@freezed
|
||||||
class DeviceConfig with _$DeviceConfig {
|
class DeviceConfig with _$DeviceConfig {
|
||||||
factory 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 .base import RpcNode, action
|
||||||
from yubikit.core import require_version, NotSupportedError
|
from yubikit.core import require_version, NotSupportedError, TRANSPORT
|
||||||
from yubikit.management import ManagementSession, DeviceConfig
|
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 dataclasses import asdict
|
||||||
|
from time import sleep
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ManagementNode(RpcNode):
|
class ManagementNode(RpcNode):
|
||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._connection_type = type(connection)
|
||||||
self.session = ManagementSession(connection)
|
self.session = ManagementSession(connection)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
@ -49,6 +58,41 @@ class ManagementNode(RpcNode):
|
|||||||
actions.remove("configure")
|
actions.remove("configure")
|
||||||
return actions
|
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
|
@action
|
||||||
def configure(self, params, event, signal):
|
def configure(self, params, event, signal):
|
||||||
reboot = params.pop("reboot", False)
|
reboot = params.pop("reboot", False)
|
||||||
@ -60,7 +104,11 @@ class ManagementNode(RpcNode):
|
|||||||
params.pop("challenge_response_timeout", None),
|
params.pop("challenge_response_timeout", None),
|
||||||
params.pop("device_flags", 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)
|
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()
|
return dict()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
Loading…
Reference in New Issue
Block a user