diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 499180b4..b429bb71 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -449,6 +449,8 @@ "slot": {} } }, + "l_generating_private_key": "Generating private key\u2026", + "s_private_key_generated": "Private key generated", "p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.", "q_delete_certificate_confirm": "Delete the certficate in PIV slot {slot}?", "@q_delete_certificate_confirm" : { @@ -525,7 +527,7 @@ "p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.", "p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.", "p_warning_piv_reset": "Warning! All data stored for PIV will be irrevocably deleted from your YubiKey.", - "p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory detault values.", + "p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory default values.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copy to clipboard", diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 3a79925f..4399427b 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -18,7 +18,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../core/models.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; @@ -46,6 +48,7 @@ class _GenerateKeyDialogState extends ConsumerState { late DateTime _validTo; late DateTime _validToDefault; late DateTime _validToMax; + bool _generating = false; @override void initState() { @@ -61,31 +64,57 @@ class _GenerateKeyDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final navigator = Navigator.of(context); + return ResponsiveDialog( + allowCancel: !_generating, title: Text(l10n.s_generate_key), actions: [ TextButton( key: keys.saveButton, - onPressed: () async { - final result = await ref - .read(pivSlotsProvider(widget.devicePath).notifier) - .generate( - widget.pivSlot.slot, - _keyType, - parameters: switch (_generateType) { - GenerateType.certificate => - PivGenerateParameters.certificate( - subject: _subject, - validFrom: _validFrom, - validTo: _validTo), - GenerateType.csr => - PivGenerateParameters.csr(subject: _subject), - }, - ); + onPressed: _generating || _subject.isEmpty + ? null + : () async { + setState(() { + _generating = true; + }); - navigator.pop(result); - }, + Function()? close; + final PivGenerateResult result; + try { + close = showMessage( + context, + l10n.l_generating_private_key, + duration: const Duration(seconds: 30), + ); + result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .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(); + } + + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(result); + showMessage( + context, + l10n.s_private_key_generated, + ); + }, + ); + }, child: Text(l10n.s_save), ), ], @@ -102,9 +131,14 @@ class _GenerateKeyDialogState extends ConsumerState { labelText: l10n.s_subject, ), textInputAction: TextInputAction.next, + enabled: !_generating, onChanged: (value) { setState(() { - _subject = value.contains('=') ? value : 'CN=$value'; + if (value.isEmpty) { + _subject = ''; + } else { + _subject = value.contains('=') ? value : 'CN=$value'; + } }); }, ), @@ -118,39 +152,45 @@ class _GenerateKeyDialogState extends ConsumerState { value: _generateType, selected: _generateType != defaultGenerateType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: (value) { - setState(() { - _generateType = value; - }); - }, + onChanged: _generating + ? null + : (value) { + setState(() { + _generateType = value; + }); + }, ), ChoiceFilterChip( items: KeyType.values, value: _keyType, selected: _keyType != defaultKeyType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: (value) { - setState(() { - _keyType = value; - }); - }, + onChanged: _generating + ? null + : (value) { + setState(() { + _keyType = value; + }); + }, ), if (_generateType == GenerateType.certificate) FilterChip( label: Text(dateFormatter.format(_validTo)), - onSelected: (value) async { - final selected = await showDatePicker( - context: context, - initialDate: _validTo, - firstDate: _validFrom, - lastDate: _validToMax, - ); - if (selected != null) { - setState(() { - _validTo = selected; - }); - } - }, + onSelected: _generating + ? null + : (value) async { + final selected = await showDatePicker( + context: context, + initialDate: _validTo, + firstDate: _validFrom, + lastDate: _validToMax, + ); + if (selected != null) { + setState(() { + _validTo = selected; + }); + } + }, ), ]), ] diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index c81232c0..3a2769fd 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -30,6 +30,8 @@ import 'reset_dialog.dart'; Widget pivBuildActions(BuildContext context, DevicePath devicePath, PivState pivState, WidgetRef ref) { + final colors = Theme.of(context).buttonTheme.colorScheme ?? + Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final usingDefaultMgmtKey = @@ -87,7 +89,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, : l10n.l_change_management_key), icon: const Icon(Icons.key_outlined), trailing: usingDefaultMgmtKey - ? const Icon(Icons.warning_amber) + ? Icon(Icons.warning_amber, color: colors.tertiary) : null, onTap: (context) { Navigator.of(context).pop(); diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 64354f20..4c307ef4 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -129,12 +129,13 @@ class _ManageKeyDialogState extends ConsumerState { final currentLenOk = protected ? _currentKeyOrPin.length >= 4 : _currentKeyOrPin.length == currentType.keyLength * 2; + final newLenOk = _keyController.text.length == hexLength; return ResponsiveDialog( title: Text(l10n.l_change_management_key), actions: [ TextButton( - onPressed: _submit, + onPressed: currentLenOk && newLenOk ? _submit : null, key: keys.saveButton, child: Text(l10n.s_save), ) @@ -232,7 +233,7 @@ class _ManageKeyDialogState extends ConsumerState { ), textInputAction: TextInputAction.next, onSubmitted: (_) { - if (_keyController.text.length == hexLength) { + if (currentLenOk && newLenOk) { _submit(); } }, diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index f61b4024..b6e9cdd5 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -160,7 +160,9 @@ class _ManagePinPukDialogState extends ConsumerState { autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: l10n.s_confirm_pin, + labelText: widget.target == ManageTarget.puk + ? l10n.s_confirm_puk + : l10n.s_confirm_pin, prefixIcon: const Icon(Icons.password_outlined), enabled: _currentPin.length >= 4 && _newPin.length >= 6, ), diff --git a/lib/theme.dart b/lib/theme.dart index 8f99e5ad..77df071b 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -21,6 +21,7 @@ const accentGreen = Color(0xff9aca3c); const primaryBlue = Color(0xff325f74); const primaryRed = Color(0xffea4335); const darkRed = Color(0xffda4d41); +const amber = Color(0xffffca28); class AppTheme { static ThemeData get lightTheme => ThemeData( @@ -32,6 +33,7 @@ class AppTheme { ).copyWith( primary: primaryBlue, //secondary: accentGreen, + tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade900), @@ -57,6 +59,7 @@ class AppTheme { //onPrimaryContainer: Colors.grey.shade100, error: darkRed, onError: Colors.white.withOpacity(0.9), + tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade500), diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 047ef6af..0b3d6d17 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -22,13 +22,16 @@ class ResponsiveDialog extends StatefulWidget { final Widget child; final List actions; final Function()? onCancel; + final bool allowCancel; - const ResponsiveDialog( - {super.key, - required this.child, - this.title, - this.actions = const [], - this.onCancel}); + const ResponsiveDialog({ + super.key, + required this.child, + this.title, + this.actions = const [], + this.onCancel, + this.allowCancel = true, + }); @override State createState() => _ResponsiveDialogState(); @@ -47,12 +50,14 @@ class _ResponsiveDialogState extends State { appBar: AppBar( title: widget.title, actions: widget.actions, - leading: CloseButton( - onPressed: () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - }, - ), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: widget.allowCancel + ? () { + widget.onCancel?.call(); + Navigator.of(context).pop(); + } + : null), ), body: SingleChildScrollView( child: SafeArea(