Improve UX while generating keys

This commit is contained in:
Dain Nilsson 2024-02-02 13:52:22 +01:00
parent 688211ddf6
commit 41f7fa2c00
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
15 changed files with 150 additions and 74 deletions

View File

@ -100,3 +100,10 @@ class ManagementNode(RpcNode):
params.pop("auto_eject_timeout"),
)
return dict()
@action(
condition=lambda self: issubclass(self._connection_type, SmartCardConnection)
)
def device_reset(self, params, event, signal):
self.session.device_reset()
return dict()

View File

@ -54,4 +54,9 @@ class _AndroidManagementStateNotifier extends ManagementStateNotifier {
ref.read(attachedDevicesProvider.notifier).refresh();
}
@override
Future<void> deviceReset() {
throw UnimplementedError();
}
}

View File

@ -405,7 +405,12 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> {
contentPadding: EdgeInsets.zero,
),
),
if (data != null && node == data.node)
if (data != null &&
node == data.node &&
resetCapabilities.any((c) =>
c.value &
(data.info.supportedCapabilities[node!.transport] ?? 0) !=
0))
PopupMenuItem(
onTap: () {
showBlurDialog(

View File

@ -27,6 +27,7 @@ import '../../desktop/models.dart';
import '../../fido/models.dart';
import '../../fido/state.dart';
import '../../management/models.dart';
import '../../management/state.dart';
import '../../oath/state.dart';
import '../../piv/state.dart';
import '../../widgets/responsive_dialog.dart';
@ -36,6 +37,12 @@ import '../state.dart';
final _log = Logger('fido.views.reset_dialog');
const resetCapabilities = [
Capability.oath,
Capability.fido2,
Capability.piv,
];
class ResetDialog extends ConsumerStatefulWidget {
final YubiKeyData data;
const ResetDialog(this.data, {super.key});
@ -75,6 +82,10 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
final enabled = widget
.data.info.config.enabledCapabilities[widget.data.node.transport] ??
0;
final isBio = [FormFactor.usbABio, FormFactor.usbCBio]
.contains(widget.data.info.formFactor);
final globalReset = isBio && (supported & Capability.piv.value) != 0;
final l10n = AppLocalizations.of(context)!;
double progress = _currentStep == -1 ? 0.0 : _currentStep / (_totalSteps);
return ResponsiveDialog(
@ -151,7 +162,18 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
showMessage(context, l10n.l_piv_app_reset);
});
},
null => null,
null => globalReset
? () async {
await ref
.read(managementStateProvider(widget.data.node.path)
.notifier)
.deviceReset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.s_factory_reset);
});
}
: null,
_ => throw UnsupportedError('Application cannot be reset'),
},
child: Text(l10n.s_reset),
@ -162,37 +184,36 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SegmentedButton<Capability>(
emptySelectionAllowed: true,
segments: [
Capability.oath,
Capability.fido2,
Capability.piv,
]
.where((c) => supported & c.value != 0)
.map((c) => ButtonSegment(
value: c,
icon: const Icon(null),
label: Padding(
padding: const EdgeInsets.only(right: 22),
child: Text(c.getDisplayName(l10n)),
),
enabled: enabled & c.value != 0,
))
.toList(),
selected: _application != null ? {_application!} : {},
onSelectionChanged: (selected) {
setState(() {
_application = selected.first;
});
},
),
if (!globalReset)
SegmentedButton<Capability>(
emptySelectionAllowed: true,
segments: resetCapabilities
.where((c) => supported & c.value != 0)
.map((c) => ButtonSegment(
value: c,
icon: const Icon(null),
label: Padding(
padding: const EdgeInsets.only(right: 22),
child: Text(c.getDisplayName(l10n)),
),
enabled: enabled & c.value != 0,
))
.toList(),
selected: _application != null ? {_application!} : {},
onSelectionChanged: (selected) {
setState(() {
_application = selected.first;
});
},
),
Text(
switch (_application) {
Capability.oath => l10n.p_warning_factory_reset,
Capability.piv => l10n.p_warning_piv_reset,
Capability.fido2 => l10n.p_warning_deletes_accounts,
_ => l10n.p_factory_reset_an_app,
_ => globalReset
? l10n.p_warning_global_reset
: l10n.p_factory_reset_an_app,
},
style: Theme.of(context)
.textTheme
@ -204,7 +225,9 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
Capability.oath => l10n.p_warning_disable_credentials,
Capability.piv => l10n.p_warning_piv_reset_desc,
Capability.fido2 => l10n.p_warning_disable_accounts,
_ => l10n.p_factory_reset_desc,
_ => globalReset
? l10n.p_warning_global_reset_desc
: l10n.p_factory_reset_desc,
},
),
if (_application == Capability.fido2 && _currentStep >= 0) ...[

View File

@ -111,4 +111,9 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
});
ref.read(attachedDevicesProvider.notifier).refresh();
}
@override
Future<void> deviceReset() async {
await _session.command('device_reset', target: ['ccid', 'management']);
}
}

View File

@ -179,7 +179,7 @@ class _$SuccessImpl implements Success {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuccessImpl &&
@ -358,7 +358,7 @@ class _$SignalImpl implements Signal {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SignalImpl &&
@ -547,7 +547,7 @@ class _$RpcErrorImpl implements RpcError {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RpcErrorImpl &&
@ -773,7 +773,7 @@ class _$RpcStateImpl implements _RpcState {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RpcStateImpl &&

View File

@ -132,7 +132,7 @@ class _$FidoStateImpl extends _FidoState {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FidoStateImpl &&
@ -265,7 +265,7 @@ class _$PinSuccessImpl implements _PinSuccess {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$PinSuccessImpl);
}
@ -392,7 +392,7 @@ class _$PinFailureImpl implements _PinFailure {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PinFailureImpl &&
@ -594,7 +594,7 @@ class _$FingerprintImpl extends _Fingerprint {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FingerprintImpl &&
@ -750,7 +750,7 @@ class _$EventCaptureImpl implements _EventCapture {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$EventCaptureImpl &&
@ -900,7 +900,7 @@ class _$EventCompleteImpl implements _EventComplete {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$EventCompleteImpl &&
@ -1040,7 +1040,7 @@ class _$EventErrorImpl implements _EventError {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$EventErrorImpl &&
@ -1274,7 +1274,7 @@ class _$FidoCredentialImpl implements _FidoCredential {
}
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FidoCredentialImpl &&

View File

@ -37,4 +37,6 @@ abstract class ManagementStateNotifier
int challengeResponseTimeout = 0,
int? autoEjectTimeout,
});
Future<void> deviceReset();
}

View File

@ -103,6 +103,8 @@ class OathPair with _$OathPair {
@freezed
class OathState with _$OathState {
const OathState._();
factory OathState(
String deviceId,
Version version, {
@ -112,6 +114,9 @@ class OathState with _$OathState {
required KeystoreState keystore,
}) = _OathState;
int? get capacity =>
version.isAtLeast(4) ? (version.isAtLeast(5, 7) ? 64 : 32) : null;
factory OathState.fromJson(Map<String, dynamic> json) =>
_$OathStateFromJson(json);
}

View File

@ -788,12 +788,13 @@ class __$$OathStateImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$OathStateImpl implements _OathState {
class _$OathStateImpl extends _OathState {
_$OathStateImpl(this.deviceId, this.version,
{required this.hasKey,
required this.remembered,
required this.locked,
required this.keystore});
required this.keystore})
: super._();
factory _$OathStateImpl.fromJson(Map<String, dynamic> json) =>
_$$OathStateImplFromJson(json);
@ -851,12 +852,13 @@ class _$OathStateImpl implements _OathState {
}
}
abstract class _OathState implements OathState {
abstract class _OathState extends OathState {
factory _OathState(final String deviceId, final Version version,
{required final bool hasKey,
required final bool remembered,
required final bool locked,
required final KeystoreState keystore}) = _$OathStateImpl;
_OathState._() : super._();
factory _OathState.fromJson(Map<String, dynamic> json) =
_$OathStateImpl.fromJson;

View File

@ -222,7 +222,7 @@ class _OathAddMultiAccountPageState
if (widget.state != null) {
final credsToAdd =
_credStates.values.where((element) => element.$1).length;
final capacity = widget.state!.version.isAtLeast(4) ? 32 : null;
final capacity = widget.state!.capacity;
return (credsToAdd > 0) &&
(capacity == null || (_numCreds! + credsToAdd <= capacity));
} else {

View File

@ -39,7 +39,7 @@ Widget oathBuildActions(
int? used,
}) {
final l10n = AppLocalizations.of(context)!;
final capacity = oathState.version.isAtLeast(4) ? 32 : null;
final capacity = oathState.capacity;
return Column(
children: [

View File

@ -28,6 +28,7 @@ const defaultKeyType = KeyType.eccp256;
const defaultGenerateType = GenerateType.certificate;
enum GenerateType {
// TODO: Add "publicKey"? Needed for X25519
certificate,
csr;
@ -116,10 +117,18 @@ enum KeyType {
rsa1024,
@JsonValue(0x07)
rsa2048,
@JsonValue(0x05)
rsa3072,
@JsonValue(0x16)
rsa4096,
@JsonValue(0x11)
eccp256,
@JsonValue(0x14)
eccp384;
eccp384,
@JsonValue(0xe0)
ed25519,
@JsonValue(0xe1)
x25519;
const KeyType();

View File

@ -71,8 +71,12 @@ Map<String, dynamic> _$$SlotMetadataImplToJson(_$SlotMetadataImpl instance) =>
const _$KeyTypeEnumMap = {
KeyType.rsa1024: 6,
KeyType.rsa2048: 7,
KeyType.rsa3072: 5,
KeyType.rsa4096: 22,
KeyType.eccp256: 17,
KeyType.eccp384: 20,
KeyType.ed25519: 224,
KeyType.x25519: 225,
};
const _$PinPolicyEnumMap = {

View File

@ -65,6 +65,18 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
_validToMax = DateTime.utc(now.year + 10, now.month, now.day);
}
List<KeyType> _getSupportedKeyTypes() => [
KeyType.rsa1024,
KeyType.rsa2048,
if (widget.pivState.version.isAtLeast(5, 7)) ...[
KeyType.rsa3072,
KeyType.rsa4096,
KeyType.ed25519,
],
KeyType.eccp256,
KeyType.eccp384,
];
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
@ -98,7 +110,6 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
final pivNotifier =
ref.read(pivSlotsProvider(widget.devicePath).notifier);
final withContext = ref.read(withContextProvider);
if (!await pivNotifier.validateRfc4514(_subject)) {
setState(() {
@ -108,31 +119,19 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
return;
}
void Function()? close;
final PivGenerateResult result;
try {
close = await withContext<void Function()>(
(context) async => showMessage(
context,
l10n.l_generating_private_key,
duration: const Duration(seconds: 30),
));
result = await pivNotifier.generate(
widget.pivSlot.slot,
_keyType,
parameters: switch (_generateType) {
GenerateType.certificate =>
PivGenerateParameters.certificate(
subject: _subject,
validFrom: _validFrom,
validTo: _validTo),
GenerateType.csr =>
PivGenerateParameters.csr(subject: _subject),
},
);
} finally {
close?.call();
}
final result = await pivNotifier.generate(
widget.pivSlot.slot,
_keyType,
parameters: switch (_generateType) {
GenerateType.certificate =>
PivGenerateParameters.certificate(
subject: _subject,
validFrom: _validFrom,
validTo: _validTo),
GenerateType.csr =>
PivGenerateParameters.csr(subject: _subject),
},
);
await ref.read(withContextProvider)(
(context) async {
@ -193,7 +192,7 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
runSpacing: 8.0,
children: [
ChoiceFilterChip<KeyType>(
items: KeyType.values,
items: _getSupportedKeyTypes(),
value: _keyType,
selected: _keyType != defaultKeyType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
@ -240,6 +239,16 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
},
),
]),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Visibility(
visible: _generating,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: const LinearProgressIndicator(),
),
),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),