PIV fixes.

This commit is contained in:
Dain Nilsson 2023-06-16 17:27:10 +02:00
parent 52bff18471
commit 7f1963d310
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
7 changed files with 115 additions and 60 deletions

View File

@ -449,6 +449,8 @@
"slot": {} "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.", "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": "Delete the certficate in PIV slot {slot}?",
"@q_delete_certificate_confirm" : { "@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_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_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": "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": {}, "@_copy_to_clipboard": {},
"l_copy_to_clipboard": "Copy to clipboard", "l_copy_to_clipboard": "Copy to clipboard",

View File

@ -18,7 +18,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart';
import '../../core/models.dart'; import '../../core/models.dart';
import '../../widgets/choice_filter_chip.dart'; import '../../widgets/choice_filter_chip.dart';
import '../../widgets/responsive_dialog.dart'; import '../../widgets/responsive_dialog.dart';
@ -46,6 +48,7 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
late DateTime _validTo; late DateTime _validTo;
late DateTime _validToDefault; late DateTime _validToDefault;
late DateTime _validToMax; late DateTime _validToMax;
bool _generating = false;
@override @override
void initState() { void initState() {
@ -61,31 +64,57 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final navigator = Navigator.of(context);
return ResponsiveDialog( return ResponsiveDialog(
allowCancel: !_generating,
title: Text(l10n.s_generate_key), title: Text(l10n.s_generate_key),
actions: [ actions: [
TextButton( TextButton(
key: keys.saveButton, key: keys.saveButton,
onPressed: () async { onPressed: _generating || _subject.isEmpty
final result = await ref ? null
.read(pivSlotsProvider(widget.devicePath).notifier) : () async {
.generate( setState(() {
widget.pivSlot.slot, _generating = true;
_keyType, });
parameters: switch (_generateType) {
GenerateType.certificate =>
PivGenerateParameters.certificate(
subject: _subject,
validFrom: _validFrom,
validTo: _validTo),
GenerateType.csr =>
PivGenerateParameters.csr(subject: _subject),
},
);
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), child: Text(l10n.s_save),
), ),
], ],
@ -102,9 +131,14 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
labelText: l10n.s_subject, labelText: l10n.s_subject,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
enabled: !_generating,
onChanged: (value) { onChanged: (value) {
setState(() { 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<GenerateKeyDialog> {
value: _generateType, value: _generateType,
selected: _generateType != defaultGenerateType, selected: _generateType != defaultGenerateType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)), itemBuilder: (value) => Text(value.getDisplayName(l10n)),
onChanged: (value) { onChanged: _generating
setState(() { ? null
_generateType = value; : (value) {
}); setState(() {
}, _generateType = value;
});
},
), ),
ChoiceFilterChip<KeyType>( ChoiceFilterChip<KeyType>(
items: KeyType.values, items: KeyType.values,
value: _keyType, value: _keyType,
selected: _keyType != defaultKeyType, selected: _keyType != defaultKeyType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)), itemBuilder: (value) => Text(value.getDisplayName(l10n)),
onChanged: (value) { onChanged: _generating
setState(() { ? null
_keyType = value; : (value) {
}); setState(() {
}, _keyType = value;
});
},
), ),
if (_generateType == GenerateType.certificate) if (_generateType == GenerateType.certificate)
FilterChip( FilterChip(
label: Text(dateFormatter.format(_validTo)), label: Text(dateFormatter.format(_validTo)),
onSelected: (value) async { onSelected: _generating
final selected = await showDatePicker( ? null
context: context, : (value) async {
initialDate: _validTo, final selected = await showDatePicker(
firstDate: _validFrom, context: context,
lastDate: _validToMax, initialDate: _validTo,
); firstDate: _validFrom,
if (selected != null) { lastDate: _validToMax,
setState(() { );
_validTo = selected; if (selected != null) {
}); setState(() {
} _validTo = selected;
}, });
}
},
), ),
]), ]),
] ]

View File

@ -30,6 +30,8 @@ import 'reset_dialog.dart';
Widget pivBuildActions(BuildContext context, DevicePath devicePath, Widget pivBuildActions(BuildContext context, DevicePath devicePath,
PivState pivState, WidgetRef ref) { PivState pivState, WidgetRef ref) {
final colors = Theme.of(context).buttonTheme.colorScheme ??
Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final usingDefaultMgmtKey = final usingDefaultMgmtKey =
@ -87,7 +89,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
: l10n.l_change_management_key), : l10n.l_change_management_key),
icon: const Icon(Icons.key_outlined), icon: const Icon(Icons.key_outlined),
trailing: usingDefaultMgmtKey trailing: usingDefaultMgmtKey
? const Icon(Icons.warning_amber) ? Icon(Icons.warning_amber, color: colors.tertiary)
: null, : null,
onTap: (context) { onTap: (context) {
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

@ -129,12 +129,13 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
final currentLenOk = protected final currentLenOk = protected
? _currentKeyOrPin.length >= 4 ? _currentKeyOrPin.length >= 4
: _currentKeyOrPin.length == currentType.keyLength * 2; : _currentKeyOrPin.length == currentType.keyLength * 2;
final newLenOk = _keyController.text.length == hexLength;
return ResponsiveDialog( return ResponsiveDialog(
title: Text(l10n.l_change_management_key), title: Text(l10n.l_change_management_key),
actions: [ actions: [
TextButton( TextButton(
onPressed: _submit, onPressed: currentLenOk && newLenOk ? _submit : null,
key: keys.saveButton, key: keys.saveButton,
child: Text(l10n.s_save), child: Text(l10n.s_save),
) )
@ -232,7 +233,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onSubmitted: (_) { onSubmitted: (_) {
if (_keyController.text.length == hexLength) { if (currentLenOk && newLenOk) {
_submit(); _submit();
} }
}, },

View File

@ -160,7 +160,9 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
autofillHints: const [AutofillHints.newPassword], autofillHints: const [AutofillHints.newPassword],
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), 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), prefixIcon: const Icon(Icons.password_outlined),
enabled: _currentPin.length >= 4 && _newPin.length >= 6, enabled: _currentPin.length >= 4 && _newPin.length >= 6,
), ),

View File

@ -21,6 +21,7 @@ const accentGreen = Color(0xff9aca3c);
const primaryBlue = Color(0xff325f74); const primaryBlue = Color(0xff325f74);
const primaryRed = Color(0xffea4335); const primaryRed = Color(0xffea4335);
const darkRed = Color(0xffda4d41); const darkRed = Color(0xffda4d41);
const amber = Color(0xffffca28);
class AppTheme { class AppTheme {
static ThemeData get lightTheme => ThemeData( static ThemeData get lightTheme => ThemeData(
@ -32,6 +33,7 @@ class AppTheme {
).copyWith( ).copyWith(
primary: primaryBlue, primary: primaryBlue,
//secondary: accentGreen, //secondary: accentGreen,
tertiary: amber.withOpacity(0.7),
), ),
textTheme: TextTheme( textTheme: TextTheme(
bodySmall: TextStyle(color: Colors.grey.shade900), bodySmall: TextStyle(color: Colors.grey.shade900),
@ -57,6 +59,7 @@ class AppTheme {
//onPrimaryContainer: Colors.grey.shade100, //onPrimaryContainer: Colors.grey.shade100,
error: darkRed, error: darkRed,
onError: Colors.white.withOpacity(0.9), onError: Colors.white.withOpacity(0.9),
tertiary: amber.withOpacity(0.7),
), ),
textTheme: TextTheme( textTheme: TextTheme(
bodySmall: TextStyle(color: Colors.grey.shade500), bodySmall: TextStyle(color: Colors.grey.shade500),

View File

@ -22,13 +22,16 @@ class ResponsiveDialog extends StatefulWidget {
final Widget child; final Widget child;
final List<Widget> actions; final List<Widget> actions;
final Function()? onCancel; final Function()? onCancel;
final bool allowCancel;
const ResponsiveDialog( const ResponsiveDialog({
{super.key, super.key,
required this.child, required this.child,
this.title, this.title,
this.actions = const [], this.actions = const [],
this.onCancel}); this.onCancel,
this.allowCancel = true,
});
@override @override
State<ResponsiveDialog> createState() => _ResponsiveDialogState(); State<ResponsiveDialog> createState() => _ResponsiveDialogState();
@ -47,12 +50,14 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
appBar: AppBar( appBar: AppBar(
title: widget.title, title: widget.title,
actions: widget.actions, actions: widget.actions,
leading: CloseButton( leading: IconButton(
onPressed: () { icon: const Icon(Icons.close),
widget.onCancel?.call(); onPressed: widget.allowCancel
Navigator.of(context).pop(); ? () {
}, widget.onCancel?.call();
), Navigator.of(context).pop();
}
: null),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: SafeArea( child: SafeArea(