diff --git a/lib/app/message.dart b/lib/app/message.dart index db7fcda8..2b2f2b41 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -32,10 +32,12 @@ Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, RouteSettings? routeSettings, + Color barrierColor = const Color(0x80000000), }) async => await showGeneralDialog( context: context, barrierDismissible: true, + barrierColor: barrierColor, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, pageBuilder: (ctx, anim1, anim2) => builder(ctx), transitionDuration: const Duration(milliseconds: 150), diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index b8ad31f8..f4e14ecd 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -224,7 +224,11 @@ class AppPage extends StatelessWidget { child: IconButton( key: actionsIconButtonKey, onPressed: () { - showBlurDialog(context: context, builder: keyActionsBuilder!); + showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: keyActionsBuilder!, + ); }, icon: keyActionsBadge ? const Badge( diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart index d76fa7de..029538f5 100644 --- a/lib/app/views/fs_dialog.dart +++ b/lib/app/views/fs_dialog.dart @@ -26,7 +26,8 @@ class FsDialog extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Dialog.fullscreen( - backgroundColor: Theme.of(context).colorScheme.background.withAlpha(100), + backgroundColor: + Theme.of(context).colorScheme.background.withOpacity(0.7), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index bf796e31..05395b53 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -59,6 +59,7 @@ class FidoUnlockedPage extends ConsumerWidget { OpenIntent: CallbackAction( onInvoke: (_) => showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => CredentialDialog(cred), )), DeleteIntent: CallbackAction( @@ -91,6 +92,7 @@ class FidoUnlockedPage extends ConsumerWidget { OpenIntent: CallbackAction( onInvoke: (_) => showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => FingerprintDialog(fp), )), EditIntent: CallbackAction( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 38bc8f36..7434e1cc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -48,6 +48,12 @@ "item": {} } }, + "s_definition": "{item}:", + "@s_definition" : { + "placeholders": { + "item": {} + } + }, "s_about": "About", "s_appearance": "Appearance", @@ -212,6 +218,12 @@ "retries": {} } }, + "l_wrong_puk_attempts_remaining": "Wrong PUK, {retries} attempt(s) remaining", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, "s_fido_pin_protection": "FIDO PIN protection", "l_fido_pin_protection_optional": "Optional FIDO PIN protection", "l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", @@ -231,6 +243,7 @@ "s_pin_required": "PIN required", "p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.", "l_piv_pin_blocked": "Blocked, use PUK to reset", + "l_piv_pin_puk_blocked": "Blocked, factory reset needed", "p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.", "@p_enter_new_piv_pin_puk" : { "placeholders": { @@ -418,32 +431,11 @@ "l_import_desc": "Import a key and/or certificate", "l_delete_certificate": "Delete certificate", "l_delete_certificate_desc": "Remove the certificate from your YubiKey", - "l_subject_issuer": "Subject: {subject}, Issuer: {issuer}", - "@l_subject_issuer" : { - "placeholders": { - "subject": {}, - "issuer": {} - } - }, - "l_serial": "Serial: {serial}", - "@l_serial" : { - "placeholders": { - "serial": {} - } - }, - "l_certificate_fingerprint": "Fingerprint: {fingerprint}", - "@l_certificate_fingerprint" : { - "placeholders": { - "fingerprint": {} - } - }, - "l_valid": "Valid: {not_before} - {not_after}", - "@l_valid" : { - "placeholders": { - "not_before": {}, - "not_after": {} - } - }, + "s_issuer": "Issuer", + "s_serial": "Serial", + "s_certificate_fingerprint": "Fingerprint", + "s_valid_from": "Valid from", + "s_valid_to": "Valid to", "l_no_certificate": "No certificate loaded", "l_key_no_certificate": "Key without certificate loaded", "s_generate_key": "Generate key", diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 3446a55b..fa771f37 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -121,40 +121,51 @@ class AccountDialog extends ConsumerWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.only(top: 48, bottom: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 32), + child: Column( children: [ - IconTheme( - data: IconTheme.of(context).copyWith(size: 24), - child: helper.buildCodeIcon(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconTheme( + data: IconTheme.of(context).copyWith(size: 24), + child: helper.buildCodeIcon(), + ), + const SizedBox(width: 8.0), + DefaultTextStyle.merge( + style: const TextStyle(fontSize: 28), + child: helper.buildCodeLabel(), + ), + ], + ), ), - const SizedBox(width: 8.0), - DefaultTextStyle.merge( - style: const TextStyle(fontSize: 28), - child: helper.buildCodeLabel(), + Text( + helper.title, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, ), + if (subtitle != null) + Text( + subtitle, + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: + Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context) + .textTheme + .bodySmall! + .color, + ), + ), ], ), ), - Text( - helper.title, - style: Theme.of(context).textTheme.headlineSmall, - softWrap: true, - textAlign: TextAlign.center, - ), - if (subtitle != null) - Text( - subtitle, - softWrap: true, - textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).textTheme.bodySmall!.color, - ), - ), - const SizedBox(height: 32), ActionListSection.fromMenuActions( context, AppLocalizations.of(context)!.s_actions, diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 8beed7dc..09222c47 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -89,6 +89,7 @@ class _AccountViewState extends ConsumerState { OpenIntent: CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => AccountDialog(credential), ); return null; diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 458dedb0..d42ab88f 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -24,7 +24,7 @@ part 'models.g.dart'; const defaultManagementKey = '010203040506070801020304050607080102030405060708'; const defaultManagementKeyType = ManagementKeyType.tdes; -const defaultKeyType = KeyType.rsa2048; +const defaultKeyType = KeyType.eccp256; const defaultGenerateType = GenerateType.certificate; enum GenerateType { diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 3a2769fd..360586b0 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -39,6 +39,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, final pinBlocked = pivState.pinAttempts == 0; final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining; + final alertIcon = Icon(Icons.warning_amber, color: colors.tertiary); return FsDialog( child: Column( @@ -50,35 +51,46 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, key: keys.managePinAction, title: l10n.s_pin, subtitle: pinBlocked - ? l10n.l_piv_pin_blocked + ? (pukAttempts != 0 + ? l10n.l_piv_pin_blocked + : l10n.l_piv_pin_puk_blocked) : l10n.l_attempts_remaining(pivState.pinAttempts), icon: const Icon(Icons.pin_outlined), - onTap: (context) { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog( - devicePath, - target: - pinBlocked ? ManageTarget.unblock : ManageTarget.pin, - ), - ); - }), + trailing: pinBlocked ? alertIcon : null, + onTap: !(pinBlocked && pukAttempts == 0) + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: pinBlocked + ? ManageTarget.unblock + : ManageTarget.pin, + ), + ); + } + : null), ActionListItem( key: keys.managePukAction, title: l10n.s_puk, subtitle: pukAttempts != null - ? l10n.l_attempts_remaining(pukAttempts) + ? (pukAttempts == 0 + ? l10n.l_piv_pin_puk_blocked + : l10n.l_attempts_remaining(pukAttempts)) : null, icon: const Icon(Icons.pin_outlined), - onTap: (context) { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog(devicePath, - target: ManageTarget.puk), - ); - }), + trailing: pukAttempts == 0 ? alertIcon : null, + onTap: pukAttempts != 0 + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog(devicePath, + target: ManageTarget.puk), + ); + } + : null), ActionListItem( key: keys.manageManagementKeyAction, title: l10n.s_management_key, @@ -88,9 +100,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, ? l10n.l_pin_protected_key : l10n.l_change_management_key), icon: const Icon(Icons.key_outlined), - trailing: usingDefaultMgmtKey - ? Icon(Icons.warning_amber, color: colors.tertiary) - : null, + trailing: usingDefaultMgmtKey ? alertIcon : null, onTap: (context) { Navigator.of(context).pop(); showBlurDialog( diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index b6e9cdd5..b45736ba 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -114,7 +114,11 @@ class _ManagePinPukDialogState extends ConsumerState { : l10n.s_current_puk, prefixIcon: const Icon(Icons.password_outlined), errorText: _currentIsWrong - ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + ? (widget.target == ManageTarget.puk + ? l10n.l_wrong_pin_attempts_remaining( + _attemptsRemaining) + : l10n.l_wrong_puk_attempts_remaining( + _attemptsRemaining)) : null, errorMaxLines: 3), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 6d3474a7..e54b879b 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -70,6 +70,7 @@ class PivScreen extends ConsumerWidget { CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => SlotDialog(e.slot), ); return null; @@ -104,7 +105,7 @@ class _CertificateListItem extends StatelessWidget { ), title: slot.getDisplayName(l10n), subtitle: certInfo != null - ? l10n.l_subject_issuer(certInfo.subject, certInfo.issuer) + ? certInfo.subject : pivSlot.hasKey == true ? l10n.l_key_no_certificate : l10n.l_no_certificate, diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index c1aa361e..92f289b5 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -1,10 +1,29 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import '../../app/message.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; +import '../../widgets/tooltip_if_truncated.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; @@ -29,6 +48,8 @@ class SlotDialog extends ConsumerWidget { final subtitleStyle = textTheme.bodyMedium!.copyWith( color: textTheme.bodySmall!.color, ); + final clipboard = ref.watch(clipboardProvider); + final withContext = ref.read(withContextProvider); final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => @@ -40,6 +61,32 @@ class SlotDialog extends ConsumerWidget { return const FsDialog(child: CircularProgressIndicator()); } + TableRow detailRow(String title, String value) { + return TableRow( + children: [ + Text( + l10n.s_definition(title), + textAlign: TextAlign.right, + ), + const SizedBox(width: 8.0), + GestureDetector( + onDoubleTap: () async { + await clipboard.setText(value); + if (!clipboard.platformGivesFeedback()) { + await withContext((context) async { + showMessage(context, l10n.p_target_copied_clipboard(title)); + }); + } + }, + child: TooltipIfTruncated( + text: value, + style: subtitleStyle, + ), + ), + ], + ); + } + final certInfo = slotData.certInfo; return registerPivActions( node.path, @@ -52,7 +99,7 @@ class SlotDialog extends ConsumerWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.only(top: 48, bottom: 32), + padding: const EdgeInsets.only(top: 48, bottom: 16), child: Column( children: [ Text( @@ -62,31 +109,29 @@ class SlotDialog extends ConsumerWidget { textAlign: TextAlign.center, ), if (certInfo != null) ...[ - Text( - l10n.l_subject_issuer( - certInfo.subject, certInfo.issuer), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_serial(certInfo.serial), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_certificate_fingerprint(certInfo.fingerprint), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_valid( - certInfo.notValidBefore, certInfo.notValidAfter), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, + Padding( + padding: const EdgeInsets.all(16), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + columnWidths: const {2: FlexColumnWidth()}, + children: [ + detailRow(l10n.s_subject, certInfo.subject), + detailRow(l10n.s_issuer, certInfo.issuer), + detailRow(l10n.s_serial, certInfo.serial), + detailRow(l10n.s_certificate_fingerprint, + certInfo.fingerprint), + detailRow( + l10n.s_valid_from, + DateFormat.yMMMEd().format( + DateTime.parse(certInfo.notValidBefore)), + ), + detailRow( + l10n.s_valid_to, + DateFormat.yMMMEd().format( + DateTime.parse(certInfo.notValidAfter)), + ), + ], + ), ), ] else ...[ Padding( @@ -98,8 +143,8 @@ class SlotDialog extends ConsumerWidget { style: subtitleStyle, ), ), + const SizedBox(height: 16), ], - const SizedBox(height: 16), ], ), ), diff --git a/lib/theme.dart b/lib/theme.dart index 77df071b..aef04bd2 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -33,14 +33,21 @@ class AppTheme { ).copyWith( primary: primaryBlue, //secondary: accentGreen, + secondary: Colors.grey.shade400, + onSecondary: Colors.grey.shade800, tertiary: amber.withOpacity(0.7), + error: darkRed, + onError: Colors.white.withOpacity(0.9), ), textTheme: TextTheme( - bodySmall: TextStyle(color: Colors.grey.shade900), + bodySmall: TextStyle(color: Colors.grey.shade600), ), dialogTheme: const DialogTheme( surfaceTintColor: Colors.white70, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); static ThemeData get darkTheme => ThemeData( @@ -52,8 +59,7 @@ class AppTheme { ).copyWith( primary: primaryGreen, //onPrimary: Colors.grey.shade900, - //secondary: accentGreen, - //secondary: const Color(0xff5d7d90), + secondary: Colors.grey.shade400, //onSecondary: Colors.grey.shade900, //primaryContainer: Colors.grey.shade800, //onPrimaryContainer: Colors.grey.shade100, @@ -67,6 +73,9 @@ class AppTheme { dialogTheme: DialogTheme( surfaceTintColor: Colors.grey.shade700, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); /* TODO: Remove this. It is left here as a reference as we adjust styles to work with Flutter 3.7. diff --git a/lib/widgets/tooltip_if_truncated.dart b/lib/widgets/tooltip_if_truncated.dart new file mode 100644 index 00000000..3694143e --- /dev/null +++ b/lib/widgets/tooltip_if_truncated.dart @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class TooltipIfTruncated extends StatelessWidget { + final String text; + final TextStyle style; + const TooltipIfTruncated( + {super.key, required this.text, required this.style}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final textWidget = Text( + text, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + style: style, + ); + final TextPainter textPainter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(minWidth: 0, maxWidth: constraints.maxWidth); + return textPainter.didExceedMaxLines + ? Tooltip( + margin: const EdgeInsets.all(16), + message: text, + child: textWidget, + ) + : textWidget; + }, + ); + } +}