This commit is contained in:
Dain Nilsson 2022-04-05 12:31:40 +02:00
commit c809bb9979
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
16 changed files with 551 additions and 357 deletions

View File

@ -47,6 +47,7 @@ class _BottomMenu extends ConsumerWidget {
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
enabled: a.action != null,
onTap: a.action == null
? null
: () {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'message_page.dart';
import 'no_device_screen.dart';
import 'device_info_screen.dart';
import '../models.dart';
@ -21,8 +22,9 @@ class MainPage extends ConsumerWidget {
}
final app = ref.watch(currentAppProvider);
if (app.getAvailability(deviceData) != Availability.enabled) {
return const Center(
child: Text('This application is disabled'),
return const MessagePage(
header: 'Application disabled',
message: 'Enable the application on your YubiKey to access',
);
}

32
lib/app/views/message_page.dart Executable file
View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'app_page.dart';
class MessagePage extends StatelessWidget {
final Widget? title;
final String header;
final String message;
final Widget? floatingActionButton;
const MessagePage({
Key? key,
this.title,
required this.header,
required this.message,
this.floatingActionButton,
}) : super(key: key);
@override
Widget build(BuildContext context) => AppPage(
title: title,
centered: true,
child: Column(
children: [
Text(header, style: Theme.of(context).textTheme.headline6),
const SizedBox(height: 12.0),
Text(message, textAlign: TextAlign.center),
],
),
floatingActionButton: floatingActionButton,
);
}

View File

@ -105,6 +105,14 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
}
}
void _submit() async {
await ref
.read(fingerprintProvider(widget.devicePath).notifier)
.renameFingerprint(_fingerprint!, _label);
Navigator.of(context).pop(true);
showMessage(context, 'Fingerprint added');
}
@override
Widget build(BuildContext context) {
// If current device changes, we need to pop back to the main Page.
@ -157,6 +165,9 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
_label = value.trim();
});
},
onFieldSubmitted: (_) {
_submit();
},
),
]
.map((e) => Padding(
@ -170,15 +181,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
},
actions: [
TextButton(
onPressed: _fingerprint != null && _label.isNotEmpty
? () async {
await ref
.read(fingerprintProvider(widget.devicePath).notifier)
.renameFingerprint(_fingerprint!, _label);
Navigator.of(context).pop(true);
showMessage(context, 'Fingerprint added');
}
: null,
onPressed: _fingerprint != null && _label.isNotEmpty ? _submit : null,
child: const Text('Save'),
),
],

View File

@ -9,6 +9,7 @@ import '../../app/views/app_failure_screen.dart';
import '../../app/views/app_loading_screen.dart';
import '../../app/views/app_page.dart';
import '../../app/views/device_avatar.dart';
import '../../app/views/message_page.dart';
import '../../desktop/state.dart';
import '../../management/models.dart';
import '../state.dart';
@ -31,11 +32,12 @@ class FidoScreen extends ConsumerWidget {
final supported = deviceData
.info.supportedCapabilities[deviceData.node.transport]!;
if (Capability.fido2.value & supported == 0) {
return AppPage(
title: const Text('WebAuthn'),
centered: true,
child: const AppFailureScreen(
'WebAuthn is supported by this device, but there are no management options available.'));
return const MessagePage(
title: Text('WebAuthn'),
header: 'No management options',
message:
'WebAuthn is supported by this device, but there are no management options available.',
);
}
if (Platform.isWindows) {
if (!ref

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart';
import '../models.dart';
import '../state.dart';
import 'pin_dialog.dart';
@ -17,38 +18,51 @@ class FidoLockedPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (!state.hasPin) {
if (state.bioEnroll != null) {
return MessagePage(
title: const Text('WebAuthn'),
header: 'No fingerprints',
message: 'Set a PIN to register fingerprints',
floatingActionButton: _buildFab(context),
);
} else {
return MessagePage(
title: const Text('WebAuthn'),
header: 'No discoverable accounts',
message:
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
floatingActionButton: _buildFab(context),
);
}
}
return AppPage(
title: const Text('WebAuthn'),
child: Column(
children: [
if (state.bioEnroll == false) ...[
const ListTile(
title: Text('Fingerprints'),
subtitle: Text('No fingerprints have been added'),
),
],
...state.hasPin
? [
const ListTile(title: Text('Unlock')),
_PinEntryForm(node.path),
]
: [
const ListTile(
title: Text('PIN'),
subtitle: Text('No PIN has been set'),
),
],
_PinEntryForm(state, node),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.pin),
);
}
FloatingActionButton _buildFab(BuildContext context) {
return FloatingActionButton.extended(
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
label: const Text('Setup'),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
onPressed: () {
showBottomMenu(context, [
if (state.bioEnroll != null)
MenuAction(
text: state.hasPin ? 'Change PIN' : 'Set PIN',
text: 'Add fingerprint',
icon: const Icon(Icons.fingerprint),
),
MenuAction(
text: 'Set PIN',
icon: const Icon(Icons.pin_outlined),
action: (context) {
showDialog(
@ -69,14 +83,15 @@ class FidoLockedPage extends ConsumerWidget {
),
]);
},
),
);
}
}
class _PinEntryForm extends ConsumerStatefulWidget {
final DevicePath _devicePath;
const _PinEntryForm(this._devicePath, {Key? key}) : super(key: key);
final FidoState _state;
final DeviceNode _deviceNode;
const _PinEntryForm(this._state, this._deviceNode, {Key? key})
: super(key: key);
@override
ConsumerState<_PinEntryForm> createState() => _PinEntryFormState();
@ -89,7 +104,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
void _submit() async {
final result = await ref
.read(fidoStateProvider(widget._devicePath).notifier)
.read(fidoStateProvider(widget._deviceNode.path).notifier)
.unlock(_pinController.text);
result.whenOrNull(failed: (retries, authBlocked) {
setState(() {
@ -115,12 +130,13 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
@override
Widget build(BuildContext context) {
final noFingerprints = widget._state.bioEnroll == false;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter the FIDO PIN for your YubiKey.'),
const Text('Enter the FIDO2 PIN for your YubiKey'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: TextField(
@ -136,9 +152,47 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
onSubmitted: (_) => _submit(),
),
),
Container(
alignment: Alignment.centerRight,
child: ElevatedButton(
Wrap(
spacing: 4.0,
runSpacing: 8.0,
children: [
OutlinedButton.icon(
icon: const Icon(Icons.pin),
label: const Text('Change PIN'),
onPressed: () {
showDialog(
context: context,
builder: (context) =>
FidoPinDialog(widget._deviceNode.path, widget._state),
);
},
),
OutlinedButton.icon(
icon: const Icon(Icons.delete),
label: const Text('Reset FIDO'),
onPressed: () {
showDialog(
context: context,
builder: (context) => ResetDialog(widget._deviceNode),
);
},
),
],
),
const SizedBox(height: 16.0),
ListTile(
leading:
noFingerprints ? const Icon(Icons.warning_amber_rounded) : null,
title: noFingerprints
? const Text(
'No fingerprints have been added',
overflow: TextOverflow.fade,
)
: null,
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
minLeadingWidth: 0,
trailing: ElevatedButton(
child: const Text('Unlock'),
onPressed:
_pinController.text.isNotEmpty && !_blocked ? _submit : null,

View File

@ -32,8 +32,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
Navigator.of(context).pop();
});
final minPinLength = widget.state.minPinLength;
final hasPin = widget.state.hasPin;
final isValid = _newPin.isNotEmpty &&
_newPin == _confirmPin &&
(!hasPin || _currentPin.isNotEmpty);
return ResponsiveDialog(
title: Text(hasPin ? 'Change PIN' : 'Set PIN'),
@ -71,7 +73,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'New password',
labelText: 'New PIN',
enabled: !hasPin || _currentPin.isNotEmpty,
errorText: _newPinError,
),
@ -86,7 +88,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Confirm password',
labelText: 'Confirm PIN',
enabled: _newPin.isNotEmpty,
),
onChanged: (value) {
@ -94,6 +96,11 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_confirmPin = value;
});
},
onFieldSubmitted: (_) {
if (isValid) {
_submit();
}
},
),
]
.map((e) => Padding(
@ -105,15 +112,18 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
actions: [
TextButton(
child: const Text('Save'),
onPressed: _newPin.isNotEmpty &&
_newPin == _confirmPin &&
(!hasPin || _currentPin.isNotEmpty)
? () async {
onPressed: isValid ? _submit : null,
),
],
);
}
void _submit() async {
final minPinLength = widget.state.minPinLength;
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
if (_newPin.length < minPinLength) {
setState(() {
_newPinError =
'New PIN must be at least $minPinLength characters';
_newPinError = 'New PIN must be at least $minPinLength characters';
});
return;
}
@ -129,15 +139,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_currentPinError =
'PIN has been blocked until the YubiKey is removed and reinserted';
} else {
_currentPinError =
'Wrong PIN ($retries tries remaining)';
_currentPinError = 'Wrong PIN ($retries tries remaining)';
}
});
});
}
: null,
),
],
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart';
import '../models.dart';
import '../state.dart';
import 'add_fingerprint_dialog.dart';
@ -21,16 +22,13 @@ class FidoUnlockedPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return AppPage(
title: const Text('WebAuthn'),
child: Column(
children: [
if (state.credMgmt) ...[
List<Widget> children = [
if (state.credMgmt)
...ref.watch(credentialProvider(node.path)).maybeWhen(
data: (creds) => creds.isNotEmpty
? [
const ListTile(title: Text('Credentials')),
...ref.watch(credentialProvider(node.path)).when(
data: (creds) => creds.isEmpty
? [const Text('You have no stored credentials')]
: creds.map((cred) => ListTile(
...creds.map((cred) => ListTile(
leading:
const CircleAvatar(child: Icon(Icons.link)),
title: Text(cred.userName),
@ -51,18 +49,16 @@ class FidoUnlockedPage extends ConsumerWidget {
],
),
)),
error: (err, trace) =>
[const Text('Failed reading credentials')],
loading: () =>
[const Center(child: CircularProgressIndicator())],
]
: [],
orElse: () => [],
),
],
if (state.bioEnroll != null) ...[
if (state.bioEnroll != null)
...ref.watch(fingerprintProvider(node.path)).maybeWhen(
data: (fingerprints) => fingerprints.isNotEmpty
? [
const ListTile(title: Text('Fingerprints')),
...ref.watch(fingerprintProvider(node.path)).when(
data: (fingerprints) => fingerprints.isEmpty
? [const Text('No fingerprints added')]
: fingerprints.map((fp) => ListTile(
...fingerprints.map((fp) => ListTile(
leading: const CircleAvatar(
child: Icon(Icons.fingerprint)),
title: Text(fp.label),
@ -91,16 +87,42 @@ class FidoUnlockedPage extends ConsumerWidget {
icon: const Icon(Icons.delete)),
],
),
)),
error: (err, trace) =>
[const Text('Failed reading fingerprints')],
loading: () =>
[const Center(child: CircularProgressIndicator())],
))
]
: [],
orElse: () => [],
),
],
],
];
if (children.isNotEmpty) {
return AppPage(
title: const Text('WebAuthn'),
child: Column(
children: children,
),
floatingActionButton: FloatingActionButton.extended(
floatingActionButton: _buildFab(context),
);
}
if (state.bioEnroll == false) {
return MessagePage(
title: const Text('WebAuthn'),
header: 'No fingerprints',
message: 'Add one or more (up to five) fingerprints',
floatingActionButton: _buildFab(context),
);
}
return MessagePage(
title: const Text('WebAuthn'),
header: 'No discoverable accounts',
message: 'Register as a Security Key on websites',
floatingActionButton: _buildFab(context),
);
}
FloatingActionButton _buildFab(BuildContext context) {
return FloatingActionButton.extended(
icon: Icon(state.bioEnroll != null ? Icons.fingerprint : Icons.pin),
label: const Text('Setup'),
backgroundColor: Theme.of(context).colorScheme.secondary,
@ -140,7 +162,6 @@ class FidoUnlockedPage extends ConsumerWidget {
),
]);
},
),
);
}
}

View File

@ -28,8 +28,8 @@ class _CapabilityForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 6.0,
runSpacing: 6.0,
spacing: 4.0,
runSpacing: 8.0,
children: Capability.values
.where((c) => capabilities & c.value != 0)
.map((c) => FilterChip(
@ -207,6 +207,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
.copyWith(enabledCapabilities: _enabled),
reboot: reboot,
);
if (!reboot) Navigator.pop(context);
showMessage(context, 'Configuration updated');
} finally {
close?.call();

View File

@ -74,8 +74,19 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
Text(subtitle ?? ''),
const SizedBox(height: 8.0),
Center(
child: Chip(
avatar: calculateReady
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
border: Border.all(width: 1.0, color: Colors.grey.shade500),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
calculateReady
? Icon(
credential.touchRequired
? Icons.touch_app
@ -89,15 +100,24 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
code.validTo * 1000,
),
),
label: Text(
if (code != null) ...[
const SizedBox(width: 8.0),
Opacity(
opacity: expired ? 0.4 : 1.0,
child: Text(
formatCode(code),
style: const TextStyle(
fontSize: 32.0,
fontFeatures: [FontFeature.tabularFigures()]),
),
),
)
],
],
),
),
),
),
],
),
actions: _buildActions(context, ref),
),

View File

@ -74,7 +74,7 @@ mixin AccountMixin {
String formatCode(OathCode? code) {
final value = code?.value;
if (value == null) {
return '••• •••';
return '';
} else if (value.length < 6) {
return value;
} else {

View File

@ -119,7 +119,12 @@ class AccountView extends ConsumerWidget with AccountMixin {
style: const TextStyle(fontSize: 18),
),
),
title: Text(title),
title: Text(
title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle!,
@ -128,10 +133,24 @@ class AccountView extends ConsumerWidget with AccountMixin {
softWrap: false,
)
: null,
trailing: Chip(
avatar: calculateReady
trailing: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
border: Border.all(width: 1.0, color: Colors.grey.shade500),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2.0),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
calculateReady
? Icon(
credential.touchRequired ? Icons.touch_app : Icons.refresh,
credential.touchRequired
? Icons.touch_app
: Icons.refresh,
size: 18,
)
: SizedBox.square(
@ -141,7 +160,10 @@ class AccountView extends ConsumerWidget with AccountMixin {
code.validTo * 1000,
),
),
label: Text(
if (code != null) const SizedBox(width: 8.0),
Opacity(
opacity: expired ? 0.4 : 1.0,
child: Text(
formatCode(code),
style: const TextStyle(
fontSize: 22.0,
@ -149,6 +171,10 @@ class AccountView extends ConsumerWidget with AccountMixin {
),
),
),
],
),
),
),
),
);
}

View File

@ -40,6 +40,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
bool _validateSecretLength = false;
_QrScanState _qrState = _QrScanState.none;
bool _isObscure = true;
List<int> _periodValues = [20, 30, 45, 60];
List<int> _digitsValues = [6, 8];
_scanQrCode(QrScanner qrScanner) async {
try {
@ -48,22 +50,28 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
});
final otpauth = await qrScanner.scanQr();
final data = CredentialData.fromUri(Uri.parse(otpauth));
_loadCredentialData(data);
} catch (e) {
setState(() {
_qrState = _QrScanState.failed;
});
}
}
_loadCredentialData(CredentialData data) {
setState(() {
_issuerController.text = data.issuer ?? '';
_accountController.text = data.name;
_secretController.text = data.secret;
_oathType = data.oathType;
_hashAlgorithm = data.hashAlgorithm;
_periodValues = [data.period];
_periodController.text = '${data.period}';
_digitsValues = [data.digits];
_digits = data.digits;
_isObscure = true;
_qrState = _QrScanState.success;
});
} catch (e) {
setState(() {
_qrState = _QrScanState.failed;
});
}
}
List<Widget> _buildQrStatus() {
@ -123,16 +131,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final b64Image = base64Encode(fileData);
final otpauth = await qrScanner.scanQr(b64Image);
final data = CredentialData.fromUri(Uri.parse(otpauth));
setState(() {
_issuerController.text = data.issuer ?? '';
_accountController.text = data.name;
_secretController.text = data.secret;
_oathType = data.oathType;
_hashAlgorithm = data.hashAlgorithm;
_periodController.text = '${data.period}';
_digits = data.digits;
_qrState = _QrScanState.success;
});
_loadCredentialData(data);
}
},
child: Column(
@ -289,7 +288,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
defaultPeriod,
isDense: true,
underline: null,
items: [20, 30, 45, 60]
items: _periodValues
.map((e) => DropdownMenuItem(
value: e,
child: Text('$e sec'),
@ -312,7 +311,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
value: _digits,
isDense: true,
underline: null,
items: [6, 7, 8]
items: _digitsValues
.map((e) => DropdownMenuItem(
value: e,
child: Text('$e digits'),

View File

@ -56,7 +56,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
},
),
Wrap(
spacing: 8.0,
spacing: 4.0,
runSpacing: 8.0,
children: [
OutlinedButton(
child: const Text('Remove password'),

View File

@ -7,6 +7,7 @@ import '../../app/models.dart';
import '../../app/views/app_failure_screen.dart';
import '../../app/views/app_loading_screen.dart';
import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart';
import '../models.dart';
import '../state.dart';
import 'account_list.dart';
@ -53,41 +54,11 @@ class _LockedView extends ConsumerWidget {
const ListTile(title: Text('Unlock')),
_UnlockForm(
devicePath,
oathState,
keystore: oathState.keystore,
),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.password),
label: const Text('Setup'),
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
onPressed: () {
showBottomMenu(context, [
MenuAction(
text: 'Change password',
icon: const Icon(Icons.password),
action: (context) {
showDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(devicePath, oathState),
);
},
),
MenuAction(
text: 'Delete all data',
icon: const Icon(Icons.delete_outline),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(devicePath),
);
},
),
]);
},
),
);
}
@ -99,7 +70,18 @@ class _UnlockedView extends ConsumerWidget {
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) => AppPage(
Widget build(BuildContext context, WidgetRef ref) {
final accounts = ref.watch(credentialListProvider(devicePath));
if (accounts?.isEmpty ?? false) {
return MessagePage(
title: const Text('Authenticator'),
header: 'No accounts',
message: 'Follow the instructions on a website to add an account',
floatingActionButton: _buildFab(context),
);
}
return AppPage(
title: Focus(
canRequestFocus: false,
onKeyEvent: (node, event) {
@ -113,7 +95,7 @@ class _UnlockedView extends ConsumerWidget {
return TextFormField(
initialValue: ref.read(searchProvider),
decoration: const InputDecoration(
hintText: 'Search accounts...',
hintText: 'Search accounts',
border: InputBorder.none,
),
onChanged: (value) {
@ -127,7 +109,12 @@ class _UnlockedView extends ConsumerWidget {
}),
),
child: AccountList(devicePath, oathState),
floatingActionButton: FloatingActionButton.extended(
floatingActionButton: _buildFab(context),
);
}
FloatingActionButton _buildFab(BuildContext context) {
final fab = FloatingActionButton.extended(
icon: const Icon(Icons.person_add_alt),
label: const Text('Setup'),
backgroundColor: Theme.of(context).colorScheme.secondary,
@ -145,7 +132,7 @@ class _UnlockedView extends ConsumerWidget {
},
),
MenuAction(
text: oathState.hasKey ? 'Change password' : 'Set password',
text: oathState.hasKey ? 'Manage password' : 'Set password',
icon: const Icon(Icons.password),
action: (context) {
showDialog(
@ -167,14 +154,17 @@ class _UnlockedView extends ConsumerWidget {
),
]);
},
),
);
return fab;
}
}
class _UnlockForm extends ConsumerStatefulWidget {
final DevicePath _devicePath;
final OathState _oathState;
final KeystoreState keystore;
const _UnlockForm(this._devicePath, {Key? key, required this.keystore})
const _UnlockForm(this._devicePath, this._oathState,
{Key? key, required this.keystore})
: super(key: key);
@override
@ -212,9 +202,10 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter the password for your YubiKey. If you don\'t know your password, you\'ll need to reset the YubiKey.',
'Enter the OATH password for your YubiKey',
),
const SizedBox(height: 16.0),
TextField(
@ -230,31 +221,68 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
onChanged: (_) => setState(() {}), // Update state on change
onSubmitted: (_) => _submit(),
),
Wrap(
spacing: 4.0,
runSpacing: 8.0,
children: [
OutlinedButton.icon(
icon: const Icon(Icons.password),
label: const Text('Manage password'),
onPressed: () {
showDialog(
context: context,
builder: (context) => ManagePasswordDialog(
widget._devicePath, widget._oathState),
);
},
),
OutlinedButton.icon(
icon: const Icon(Icons.delete),
label: const Text('Reset OATH'),
onPressed: () {
showDialog(
context: context,
builder: (context) => ResetDialog(widget._devicePath),
);
},
),
],
),
],
),
),
CheckboxListTile(
const SizedBox(height: 16.0),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: keystoreFailed
? const ListTile(
leading: Icon(Icons.warning_amber_rounded),
title: Text('OS Keystore unavailable'),
dense: true,
minLeadingWidth: 0,
)
: CheckboxListTile(
title: const Text('Remember password'),
subtitle: Text(keystoreFailed
? 'The OS keychain is not available.'
: 'Uses the OS keychain to protect access to this YubiKey.'),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
value: _remember,
onChanged: keystoreFailed
? null
: (value) {
onChanged: (value) {
setState(() {
_remember = value ?? false;
});
},
),
Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.centerRight,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton(
child: const Text('Unlock'),
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
),
)
],
),
],
);

View File

@ -33,7 +33,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
centerTitle: true,
title: widget.title,
actions: widget.actions,
leading: BackButton(
leading: CloseButton(
onPressed: () {
widget.onCancel?.call();
Navigator.of(context).pop();