mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-26 10:33:15 +03:00
Merge branch 'main' into adamve/android_fido
This commit is contained in:
commit
0310919607
@ -56,9 +56,9 @@ Future<Widget> initialize() async {
|
||||
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
supportedAppsProvider.overrideWith(
|
||||
supportedSectionsProvider.overrideWith(
|
||||
(ref) {
|
||||
return [Application.home, Application.accounts, Application.passkeys];
|
||||
return [Section.home, Section.accounts, Section.passkeys];
|
||||
},
|
||||
),
|
||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
|
||||
@ -72,10 +72,10 @@ Future<Widget> initialize() async {
|
||||
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
|
||||
credentialListProvider
|
||||
.overrideWithProvider(androidCredentialListProvider.call),
|
||||
currentAppProvider.overrideWith((ref) {
|
||||
final notifier = AndroidSubPageNotifier(
|
||||
currentSectionProvider.overrideWith((ref) {
|
||||
final notifier = AndroidSectionNotifier(
|
||||
ref,
|
||||
ref.watch(supportedAppsProvider),
|
||||
ref.watch(supportedSectionsProvider),
|
||||
ref.watch(prefProvider),
|
||||
);
|
||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider,
|
||||
|
@ -91,25 +91,25 @@ final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||
});
|
||||
|
||||
class _AndroidAppContextHandler {
|
||||
Future<void> switchAppContext(Application subPage) async {
|
||||
await _contextChannel.invokeMethod('setContext', {'index': subPage.index});
|
||||
Future<void> switchAppContext(Section section) async {
|
||||
await _contextChannel.invokeMethod('setContext', {'index': section.index});
|
||||
}
|
||||
}
|
||||
|
||||
final androidAppContextHandler =
|
||||
Provider<_AndroidAppContextHandler>((ref) => _AndroidAppContextHandler());
|
||||
|
||||
class AndroidSubPageNotifier extends CurrentAppNotifier {
|
||||
final StateNotifierProviderRef<CurrentAppNotifier, Application> _ref;
|
||||
class AndroidSectionNotifier extends CurrentSectionNotifier {
|
||||
final StateNotifierProviderRef<CurrentSectionNotifier, Section> _ref;
|
||||
|
||||
AndroidSubPageNotifier(this._ref, super.supportedApps, super.prefs) {
|
||||
AndroidSectionNotifier(this._ref, super._supportedSections, super.prefs) {
|
||||
_ref.read(androidAppContextHandler).switchAppContext(state);
|
||||
}
|
||||
|
||||
@override
|
||||
void setCurrentApp(Application app) {
|
||||
super.setCurrentApp(app);
|
||||
_ref.read(androidAppContextHandler).switchAppContext(app);
|
||||
void setCurrentSection(Section section) {
|
||||
super.setCurrentSection(section);
|
||||
_ref.read(androidAppContextHandler).switchAppContext(section);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -16,11 +16,12 @@
|
||||
|
||||
import '../core/state.dart';
|
||||
|
||||
final home = root.feature('home');
|
||||
final oath = root.feature('oath');
|
||||
final fido = root.feature('fido');
|
||||
final piv = root.feature('piv');
|
||||
final otp = root.feature('otp');
|
||||
|
||||
final management = root.feature('management');
|
||||
final home = root.feature('home');
|
||||
|
||||
final fingerprints = fido.feature('fingerprints');
|
||||
|
@ -31,43 +31,32 @@ const _listEquality = ListEquality();
|
||||
|
||||
enum Availability { enabled, disabled, unsupported }
|
||||
|
||||
enum Application {
|
||||
enum Section {
|
||||
home(),
|
||||
accounts([Capability.oath]),
|
||||
webauthn([Capability.u2f]),
|
||||
securityKey([Capability.u2f]),
|
||||
fingerprints([Capability.fido2]),
|
||||
passkeys([Capability.fido2]),
|
||||
certificates([Capability.piv]),
|
||||
slots([Capability.otp]),
|
||||
management();
|
||||
slots([Capability.otp]);
|
||||
|
||||
final List<Capability> capabilities;
|
||||
|
||||
const Application([this.capabilities = const []]);
|
||||
const Section([this.capabilities = const []]);
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
Application.home => l10n.s_home,
|
||||
Application.accounts => l10n.s_accounts,
|
||||
Application.webauthn => l10n.s_webauthn,
|
||||
Application.fingerprints => l10n.s_fingerprints,
|
||||
Application.passkeys => l10n.s_passkeys,
|
||||
Application.certificates => l10n.s_certificates,
|
||||
Application.slots => l10n.s_slots,
|
||||
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
|
||||
Section.home => l10n.s_home,
|
||||
Section.accounts => l10n.s_accounts,
|
||||
Section.securityKey => l10n.s_security_key,
|
||||
Section.fingerprints => l10n.s_fingerprints,
|
||||
Section.passkeys => l10n.s_passkeys,
|
||||
Section.certificates => l10n.s_certificates,
|
||||
Section.slots => l10n.s_slots,
|
||||
};
|
||||
|
||||
Availability getAvailability(YubiKeyData data) {
|
||||
if (this == Application.management) {
|
||||
final version = data.info.version;
|
||||
final available = (version.major > 4 || // YK5 and up
|
||||
(version.major == 4 && version.minor >= 1) || // YK4.1 and up
|
||||
version.major == 3); // NEO
|
||||
// Management can't be disabled
|
||||
return available ? Availability.enabled : Availability.unsupported;
|
||||
}
|
||||
|
||||
// TODO: Require credman for passkeys?
|
||||
if (this == Application.fingerprints) {
|
||||
if (this == Section.fingerprints) {
|
||||
if (!const {FormFactor.usbABio, FormFactor.usbCBio}
|
||||
.contains(data.info.formFactor)) {
|
||||
return Availability.unsupported;
|
||||
@ -79,8 +68,8 @@ enum Application {
|
||||
final int enabled =
|
||||
data.info.config.enabledCapabilities[data.node.transport] ?? 0;
|
||||
|
||||
// Don't show WebAuthn if we have FIDO2
|
||||
if (this == Application.webauthn &&
|
||||
// Don't show securityKey if we have FIDO2
|
||||
if (this == Section.securityKey &&
|
||||
Capability.fido2.value & supported != 0) {
|
||||
return Availability.unsupported;
|
||||
}
|
||||
|
@ -38,23 +38,24 @@ const officialLocales = [
|
||||
Locale('en', ''),
|
||||
];
|
||||
|
||||
extension on Application {
|
||||
extension on Section {
|
||||
Feature get _feature => switch (this) {
|
||||
Application.home => features.home,
|
||||
Application.accounts => features.oath,
|
||||
Application.webauthn => features.fido,
|
||||
Application.passkeys => features.fido,
|
||||
Application.fingerprints => features.fingerprints,
|
||||
Application.slots => features.otp,
|
||||
Application.certificates => features.piv,
|
||||
Application.management => features.management,
|
||||
Section.home => features.home,
|
||||
Section.accounts => features.oath,
|
||||
Section.securityKey => features.fido,
|
||||
Section.passkeys => features.fido,
|
||||
Section.fingerprints => features.fingerprints,
|
||||
Section.slots => features.otp,
|
||||
Section.certificates => features.piv,
|
||||
};
|
||||
}
|
||||
|
||||
final supportedAppsProvider = Provider<List<Application>>(
|
||||
final supportedSectionsProvider = Provider<List<Section>>(
|
||||
(ref) {
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return Application.values.where((app) => hasFeature(app._feature)).toList();
|
||||
return Section.values
|
||||
.where((section) => hasFeature(section._feature))
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
|
||||
@ -201,62 +202,62 @@ abstract class CurrentDeviceNotifier extends Notifier<DeviceNode?> {
|
||||
setCurrentDevice(DeviceNode? device);
|
||||
}
|
||||
|
||||
final currentAppProvider =
|
||||
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
|
||||
final notifier = CurrentAppNotifier(
|
||||
ref.watch(supportedAppsProvider), ref.watch(prefProvider));
|
||||
final currentSectionProvider =
|
||||
StateNotifierProvider<CurrentSectionNotifier, Section>((ref) {
|
||||
final notifier = CurrentSectionNotifier(
|
||||
ref.watch(supportedSectionsProvider), ref.watch(prefProvider));
|
||||
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
|
||||
notifier.notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
|
||||
}, fireImmediately: true);
|
||||
return notifier;
|
||||
});
|
||||
|
||||
class CurrentAppNotifier extends StateNotifier<Application> {
|
||||
final List<Application> _supportedApps;
|
||||
static const String _key = 'APP_STATE_LAST_APP';
|
||||
class CurrentSectionNotifier extends StateNotifier<Section> {
|
||||
final List<Section> _supportedSections;
|
||||
static const String _key = 'APP_STATE_LAST_SECTION';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
CurrentAppNotifier(this._supportedApps, this._prefs)
|
||||
: super(_fromName(_prefs.getString(_key), _supportedApps));
|
||||
CurrentSectionNotifier(this._supportedSections, this._prefs)
|
||||
: super(_fromName(_prefs.getString(_key), _supportedSections));
|
||||
|
||||
void setCurrentApp(Application app) {
|
||||
state = app;
|
||||
_prefs.setString(_key, app.name);
|
||||
void setCurrentSection(Section section) {
|
||||
state = section;
|
||||
_prefs.setString(_key, section.name);
|
||||
}
|
||||
|
||||
void notifyDeviceChanged(YubiKeyData? data) {
|
||||
if (data == null) {
|
||||
state = _supportedApps.first;
|
||||
state = _supportedSections.first;
|
||||
return;
|
||||
}
|
||||
|
||||
String? lastAppName = _prefs.getString(_key);
|
||||
if (lastAppName != null && lastAppName != state.name) {
|
||||
// Try switching to saved app
|
||||
state = Application.values.firstWhere((app) => app.name == lastAppName);
|
||||
state = Section.values.firstWhere((app) => app.name == lastAppName);
|
||||
}
|
||||
if (state == Application.passkeys &&
|
||||
if (state == Section.passkeys &&
|
||||
state.getAvailability(data) != Availability.enabled) {
|
||||
state = Application.webauthn;
|
||||
state = Section.securityKey;
|
||||
}
|
||||
if (state == Application.webauthn &&
|
||||
if (state == Section.securityKey &&
|
||||
state.getAvailability(data) != Availability.enabled) {
|
||||
state = Application.passkeys;
|
||||
state = Section.passkeys;
|
||||
}
|
||||
if (state.getAvailability(data) != Availability.unsupported) {
|
||||
// Keep current app
|
||||
return;
|
||||
}
|
||||
|
||||
state = _supportedApps.firstWhere(
|
||||
state = _supportedSections.firstWhere(
|
||||
(app) => app.getAvailability(data) == Availability.enabled,
|
||||
orElse: () => _supportedApps.first,
|
||||
orElse: () => _supportedSections.first,
|
||||
);
|
||||
}
|
||||
|
||||
static Application _fromName(String? name, List<Application> supportedApps) =>
|
||||
supportedApps.firstWhere((element) => element.name == name,
|
||||
orElse: () => supportedApps.first);
|
||||
static Section _fromName(String? name, List<Section> supportedSections) =>
|
||||
supportedSections.firstWhere((element) => element.name == name,
|
||||
orElse: () => supportedSections.first);
|
||||
}
|
||||
|
||||
abstract class QrScanner {
|
||||
|
@ -64,9 +64,9 @@ class AppFailurePage extends ConsumerWidget {
|
||||
case 'fido':
|
||||
if (Platform.isWindows &&
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
final currentApp = ref.read(currentAppProvider);
|
||||
title = currentApp.getDisplayName(l10n);
|
||||
capabilities = currentApp.capabilities;
|
||||
final currentSection = ref.read(currentSectionProvider);
|
||||
title = currentSection.getDisplayName(l10n);
|
||||
capabilities = currentSection.capabilities;
|
||||
header = l10n.l_admin_privileges_required;
|
||||
message = l10n.p_webauthn_elevated_permissions_required;
|
||||
centered = false;
|
||||
|
@ -39,9 +39,9 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
if (pid.usbInterfaces == UsbInterface.fido.value) {
|
||||
if (Platform.isWindows &&
|
||||
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
|
||||
final currentApp = ref.read(currentAppProvider);
|
||||
final currentSection = ref.read(currentSectionProvider);
|
||||
return HomeMessagePage(
|
||||
capabilities: currentApp.capabilities,
|
||||
capabilities: currentSection.capabilities,
|
||||
header: l10n.l_admin_privileges_required,
|
||||
message: l10n.p_elevated_permissions_required,
|
||||
actionsBuilder: (context, expanded) => [
|
||||
|
@ -118,8 +118,8 @@ class MainPage extends ConsumerWidget {
|
||||
} else {
|
||||
return ref.watch(currentDeviceDataProvider).when(
|
||||
data: (data) {
|
||||
final app = ref.watch(currentAppProvider);
|
||||
final capabilities = app.capabilities;
|
||||
final section = ref.watch(currentSectionProvider);
|
||||
final capabilities = section.capabilities;
|
||||
if (data.info.supportedCapabilities.isEmpty &&
|
||||
data.name == 'Unrecognized device') {
|
||||
return HomeMessagePage(
|
||||
@ -131,19 +131,20 @@ class MainPage extends ConsumerWidget {
|
||||
),
|
||||
header: l10n.s_yk_not_recognized,
|
||||
);
|
||||
} else if (app.getAvailability(data) ==
|
||||
} else if (section.getAvailability(data) ==
|
||||
Availability.unsupported) {
|
||||
return MessagePage(
|
||||
title: app.getDisplayName(l10n),
|
||||
title: section.getDisplayName(l10n),
|
||||
capabilities: capabilities,
|
||||
header: l10n.s_app_not_supported,
|
||||
message: l10n.l_app_not_supported_on_yk(capabilities
|
||||
.map((c) => c.getDisplayName(l10n))
|
||||
.join(',')),
|
||||
);
|
||||
} else if (app.getAvailability(data) != Availability.enabled) {
|
||||
} else if (section.getAvailability(data) !=
|
||||
Availability.enabled) {
|
||||
return MessagePage(
|
||||
title: app.getDisplayName(l10n),
|
||||
title: section.getDisplayName(l10n),
|
||||
capabilities: capabilities,
|
||||
header: l10n.s_app_disabled,
|
||||
message: l10n.l_app_disabled_desc(capabilities
|
||||
@ -166,18 +167,14 @@ class MainPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return switch (app) {
|
||||
Application.home => HomeScreen(data),
|
||||
Application.accounts => OathScreen(data.node.path),
|
||||
Application.webauthn => const WebAuthnScreen(),
|
||||
Application.passkeys => PasskeysScreen(data),
|
||||
Application.fingerprints => FingerprintsScreen(data),
|
||||
Application.certificates => PivScreen(data.node.path),
|
||||
Application.slots => OtpScreen(data.node.path),
|
||||
_ => MessagePage(
|
||||
header: l10n.s_app_not_supported,
|
||||
message: l10n.l_app_not_supported_desc,
|
||||
),
|
||||
return switch (section) {
|
||||
Section.home => HomeScreen(data),
|
||||
Section.accounts => OathScreen(data.node.path),
|
||||
Section.securityKey => const WebAuthnScreen(),
|
||||
Section.passkeys => PasskeysScreen(data),
|
||||
Section.fingerprints => FingerprintsScreen(data),
|
||||
Section.certificates => PivScreen(data.node.path),
|
||||
Section.slots => OtpScreen(data.node.path),
|
||||
};
|
||||
},
|
||||
loading: () => DeviceErrorScreen(deviceNode),
|
||||
|
@ -85,27 +85,25 @@ class NavigationItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
extension on Application {
|
||||
extension on Section {
|
||||
IconData get _icon => switch (this) {
|
||||
Application.accounts => Symbols.supervisor_account,
|
||||
Application.webauthn => Symbols.security_key,
|
||||
Application.passkeys => Symbols.passkey,
|
||||
Application.fingerprints => Symbols.fingerprint,
|
||||
Application.slots => Symbols.touch_app,
|
||||
Application.certificates => Symbols.approval,
|
||||
Application.management => Symbols.construction,
|
||||
Application.home => Symbols.home
|
||||
Section.home => Symbols.home,
|
||||
Section.accounts => Symbols.supervisor_account,
|
||||
Section.securityKey => Symbols.security_key,
|
||||
Section.passkeys => Symbols.passkey,
|
||||
Section.fingerprints => Symbols.fingerprint,
|
||||
Section.slots => Symbols.touch_app,
|
||||
Section.certificates => Symbols.badge,
|
||||
};
|
||||
|
||||
Key get _key => switch (this) {
|
||||
Application.accounts => oathAppDrawer,
|
||||
Application.webauthn => u2fAppDrawer,
|
||||
Application.passkeys => fidoPasskeysAppDrawer,
|
||||
Application.fingerprints => fidoFingerprintsAppDrawer,
|
||||
Application.slots => otpAppDrawer,
|
||||
Application.certificates => pivAppDrawer,
|
||||
Application.management => managementAppDrawer,
|
||||
Application.home => homeDrawer,
|
||||
Section.home => homeDrawer,
|
||||
Section.accounts => oathAppDrawer,
|
||||
Section.securityKey => u2fAppDrawer,
|
||||
Section.passkeys => fidoPasskeysAppDrawer,
|
||||
Section.fingerprints => fidoFingerprintsAppDrawer,
|
||||
Section.slots => otpAppDrawer,
|
||||
Section.certificates => pivAppDrawer,
|
||||
};
|
||||
}
|
||||
|
||||
@ -118,17 +116,16 @@ class NavigationContent extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final supportedApps = ref.watch(supportedAppsProvider);
|
||||
final supportedSections = ref.watch(supportedSectionsProvider);
|
||||
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
|
||||
|
||||
final availableApps = data != null
|
||||
? supportedApps
|
||||
.where(
|
||||
(app) => app.getAvailability(data) != Availability.unsupported)
|
||||
final availableSections = data != null
|
||||
? supportedSections
|
||||
.where((section) =>
|
||||
section.getAvailability(data) != Availability.unsupported)
|
||||
.toList()
|
||||
: [Application.home];
|
||||
availableApps.remove(Application.management);
|
||||
final currentApp = ref.watch(currentAppProvider);
|
||||
: [Section.home];
|
||||
final currentSection = ref.watch(currentSectionProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@ -144,21 +141,21 @@ class NavigationContent extends ConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
// Normal YubiKey Applications
|
||||
...availableApps.map((app) => NavigationItem(
|
||||
...availableSections.map((app) => NavigationItem(
|
||||
key: app._key,
|
||||
title: app.getDisplayName(l10n),
|
||||
leading:
|
||||
Icon(app._icon, fill: app == currentApp ? 1.0 : 0.0),
|
||||
leading: Icon(app._icon,
|
||||
fill: app == currentSection ? 1.0 : 0.0),
|
||||
collapsed: !extended,
|
||||
selected: app == currentApp,
|
||||
onTap: data == null && currentApp == Application.home ||
|
||||
selected: app == currentSection,
|
||||
onTap: data == null && currentSection == Section.home ||
|
||||
data != null &&
|
||||
app.getAvailability(data) ==
|
||||
Availability.enabled
|
||||
? () {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(app);
|
||||
.read(currentSectionProvider.notifier)
|
||||
.setCurrentSection(app);
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ extension on Capability {
|
||||
IconData get _icon => switch (this) {
|
||||
Capability.oath => Symbols.supervisor_account,
|
||||
Capability.fido2 => Symbols.passkey,
|
||||
Capability.piv => Symbols.approval,
|
||||
Capability.piv => Symbols.badge,
|
||||
_ => throw UnsupportedError('Icon not defined'),
|
||||
};
|
||||
}
|
||||
@ -256,10 +256,10 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
if (isAndroid) {
|
||||
// switch current app context
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(switch (_application) {
|
||||
Capability.oath => Application.accounts,
|
||||
Capability.fido2 => Application.passkeys,
|
||||
.read(currentSectionProvider.notifier)
|
||||
.setCurrentSection(switch (_application) {
|
||||
Capability.oath => Section.accounts,
|
||||
Capability.fido2 => Section.passkeys,
|
||||
_ => throw UnimplementedError(
|
||||
'Reset for $_application is not implemented')
|
||||
});
|
||||
|
@ -32,14 +32,18 @@ import '../state.dart';
|
||||
|
||||
final _log = Logger('desktop.fido.state');
|
||||
|
||||
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
|
||||
(ref, _) => null,
|
||||
final _pinProvider = StateProvider.family<String?, DevicePath>(
|
||||
(ref, _) {
|
||||
// Clear PIN if current device is changed
|
||||
ref.watch(currentDeviceProvider);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final _sessionProvider =
|
||||
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
// Make sure the pinProvider is held for the duration of the session.
|
||||
// Refresh state when PIN is changed
|
||||
ref.watch(_pinProvider(devicePath));
|
||||
return RpcNodeSession(
|
||||
ref.watch(rpcProvider).requireValue, devicePath, ['fido', 'ctap2']);
|
||||
|
@ -256,7 +256,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
)
|
||||
]
|
||||
],
|
||||
|
@ -112,8 +112,8 @@ class _FidoLockedPage extends ConsumerWidget {
|
||||
label: Text(l10n.s_setup_fingerprints),
|
||||
onPressed: () async {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(Application.fingerprints);
|
||||
.read(currentSectionProvider.notifier)
|
||||
.setCurrentSection(Section.fingerprints);
|
||||
},
|
||||
avatar: const Icon(Symbols.fingerprint),
|
||||
),
|
||||
@ -248,8 +248,8 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
label: Text(l10n.s_setup_fingerprints),
|
||||
onPressed: () async {
|
||||
ref
|
||||
.read(currentAppProvider.notifier)
|
||||
.setCurrentApp(Application.fingerprints);
|
||||
.read(currentSectionProvider.notifier)
|
||||
.setCurrentSection(Section.fingerprints);
|
||||
},
|
||||
avatar: const Icon(Symbols.fingerprint),
|
||||
)
|
||||
|
@ -46,7 +46,8 @@ class FidoPinDialog extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
String _currentPin = '';
|
||||
final _currentPinController = TextEditingController();
|
||||
final _currentPinFocus = FocusNode();
|
||||
String _newPin = '';
|
||||
String _confirmPin = '';
|
||||
String? _currentPinError;
|
||||
@ -56,15 +57,28 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
bool _isObscureCurrent = true;
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
bool _isBlocked = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPinController.dispose();
|
||||
_currentPinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasPin = widget.state.hasPin;
|
||||
final isValid = _newPin.isNotEmpty &&
|
||||
_newPin == _confirmPin &&
|
||||
(!hasPin || _currentPin.isNotEmpty);
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
final currentMinPinLen = !hasPin
|
||||
? 0
|
||||
// N.B. current PIN may be shorter than minimum if set before the minimum was increased
|
||||
: (widget.state.forcePinChange ? 4 : widget.state.minPinLength);
|
||||
final currentPinLenOk =
|
||||
_currentPinController.text.length >= currentMinPinLen;
|
||||
final newPinLenOk = _newPin.length >= minPinLength;
|
||||
final isValid = currentPinLenOk && newPinLenOk && _newPin == _confirmPin;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
||||
@ -84,11 +98,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
Text(l10n.p_enter_current_pin_or_reset_no_puk),
|
||||
AppTextFormField(
|
||||
key: currentPin,
|
||||
initialValue: _currentPin,
|
||||
controller: _currentPinController,
|
||||
focusNode: _currentPinFocus,
|
||||
autofocus: true,
|
||||
obscureText: _isObscureCurrent,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: AppInputDecoration(
|
||||
enabled: !_isBlocked,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_current_pin,
|
||||
errorText: _currentIsWrong ? _currentPinError : null,
|
||||
@ -110,10 +126,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPin = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
],
|
||||
Text(l10n.p_enter_new_fido2_pin(minPinLength)),
|
||||
// TODO: Set max characters based on UTF-8 bytes
|
||||
@ -126,7 +141,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_new_pin,
|
||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||
enabled: !_isBlocked && currentPinLenOk,
|
||||
errorText: _newIsWrong ? _newPinError : null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
@ -148,7 +163,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_newPin = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextFormField(
|
||||
key: confirmPin,
|
||||
initialValue: _confirmPin,
|
||||
@ -170,8 +185,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
tooltip:
|
||||
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
),
|
||||
enabled:
|
||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
||||
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
|
||||
errorText: _newPin.length == _confirmPin.length &&
|
||||
_newPin != _confirmPin
|
||||
? l10n.l_pin_mismatch
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -183,7 +202,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@ -197,15 +216,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
|
||||
void _submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
|
||||
if (_newPin.length < minPinLength) {
|
||||
setState(() {
|
||||
_newPinError = l10n.l_new_pin_len(minPinLength);
|
||||
_newIsWrong = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final oldPin = _currentPinController.text.isNotEmpty
|
||||
? _currentPinController.text
|
||||
: null;
|
||||
try {
|
||||
final result = await ref
|
||||
.read(fidoStateProvider(widget.devicePath).notifier)
|
||||
@ -215,9 +228,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
showMessage(context, l10n.s_pin_set);
|
||||
}, failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
_currentPinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||
_currentPinFocus.requestFocus();
|
||||
if (authBlocked) {
|
||||
_currentPinError = l10n.l_pin_soft_locked;
|
||||
_currentIsWrong = true;
|
||||
_isBlocked = true;
|
||||
} else {
|
||||
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
|
||||
_currentIsWrong = true;
|
||||
|
@ -38,11 +38,19 @@ class PinEntryForm extends ConsumerStatefulWidget {
|
||||
|
||||
class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinFocus = FocusNode();
|
||||
bool _blocked = false;
|
||||
int? _retries;
|
||||
bool _pinIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_pinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_pinIsWrong = false;
|
||||
@ -53,8 +61,10 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
.read(fidoStateProvider(widget._deviceNode.path).notifier)
|
||||
.unlock(_pinController.text);
|
||||
result.whenOrNull(failed: (retries, authBlocked) {
|
||||
_pinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||
_pinFocus.requestFocus();
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_pinIsWrong = true;
|
||||
_retries = retries;
|
||||
_blocked = authBlocked;
|
||||
@ -97,6 +107,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _pinController,
|
||||
focusNode: _pinFocus,
|
||||
enabled: !_blocked && (_retries ?? 1) > 0,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
@ -121,7 +133,7 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
});
|
||||
}, // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
),
|
||||
ListTile(
|
||||
leading: noFingerprints
|
||||
@ -141,8 +153,12 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
key: unlockFido2WithPin,
|
||||
icon: const Icon(Symbols.lock_open),
|
||||
label: Text(l10n.s_unlock),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
onPressed: !_pinIsWrong &&
|
||||
_pinController.text.length >=
|
||||
widget._state.minPinLength &&
|
||||
!_blocked
|
||||
? _submit
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -112,7 +112,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -27,7 +27,7 @@ class WebAuthnScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return MessagePage(
|
||||
title: l10n.s_webauthn,
|
||||
title: l10n.s_security_key,
|
||||
capabilities: const [Capability.u2f],
|
||||
header: l10n.l_ready_to_use,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
|
@ -24,9 +24,9 @@ import '../../app/features.dart' as features;
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/reset_dialog.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
|
||||
@ -34,11 +34,13 @@ Widget homeBuildActions(
|
||||
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
final managementAvailability =
|
||||
deviceData == null || !hasFeature(features.management)
|
||||
? Availability.unsupported
|
||||
: Application.management.getAvailability(deviceData);
|
||||
final managementAvailability = hasFeature(features.management) &&
|
||||
switch (deviceData?.info.version) {
|
||||
Version version => (version.major > 4 || // YK5 and up
|
||||
(version.major == 4 && version.minor >= 1) || // YK4.1 and up
|
||||
version.major == 3), // NEO,
|
||||
null => false,
|
||||
};
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@ -46,7 +48,7 @@ Widget homeBuildActions(
|
||||
ActionListSection(
|
||||
l10n.s_device,
|
||||
children: [
|
||||
if (managementAvailability == Availability.enabled)
|
||||
if (managementAvailability)
|
||||
ActionListItem(
|
||||
feature: features.management,
|
||||
icon: const Icon(Symbols.construction),
|
||||
@ -57,12 +59,11 @@ Widget homeBuildActions(
|
||||
subtitle: deviceData.info.version.major > 4
|
||||
? l10n.l_toggle_applications_desc
|
||||
: l10n.l_toggle_interfaces_desc,
|
||||
onTap: (context) async {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(deviceData),
|
||||
),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(deviceData),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -77,12 +78,11 @@ Widget homeBuildActions(
|
||||
title: l10n.s_factory_reset,
|
||||
subtitle: l10n.l_factory_reset_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (context) async {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(deviceData),
|
||||
),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(deviceData),
|
||||
);
|
||||
},
|
||||
)
|
||||
@ -94,7 +94,8 @@ Widget homeBuildActions(
|
||||
title: l10n.s_settings,
|
||||
subtitle: l10n.l_settings_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (_) {
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Actions.maybeInvoke(context, const SettingsIntent());
|
||||
},
|
||||
),
|
||||
@ -103,7 +104,8 @@ Widget homeBuildActions(
|
||||
title: l10n.s_help_and_about,
|
||||
subtitle: l10n.l_help_and_about_desc,
|
||||
actionStyle: ActionStyle.primary,
|
||||
onTap: (_) {
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Actions.maybeInvoke(context, const AboutIntent());
|
||||
},
|
||||
)
|
||||
|
@ -87,7 +87,7 @@ class _ManageLabelDialogState extends ConsumerState<ManageLabelDialog> {
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
)
|
||||
).init()
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -66,7 +66,7 @@
|
||||
"s_settings": "Einstellungen",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": null,
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_security_key": null,
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "Hilfe und Über",
|
||||
"l_help_and_about_desc": null,
|
||||
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "PIN bestätigen",
|
||||
"s_confirm_puk": null,
|
||||
"s_unblock_pin": null,
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Neue PIN muss mindestens {length} Zeichen lang sein",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Neues Passwort",
|
||||
"s_current_password": "Aktuelles Passwort",
|
||||
"s_confirm_password": "Passwort bestätigen",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Falsches Passwort",
|
||||
"s_remove_password": "Passwort entfernen",
|
||||
"s_password_removed": "Passwort entfernt",
|
||||
|
@ -66,7 +66,7 @@
|
||||
"s_settings": "Settings",
|
||||
"l_settings_desc": "Change application preferences",
|
||||
"s_certificates": "Certificates",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_security_key": "Security Key",
|
||||
"s_slots": "Slots",
|
||||
"s_help_and_about": "Help and about",
|
||||
"l_help_and_about_desc": "Troubleshoot and support",
|
||||
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Confirm PIN",
|
||||
"s_confirm_puk": "Confirm PUK",
|
||||
"s_unblock_pin": "Unblock PIN",
|
||||
"l_pin_mismatch": "PINs do not match",
|
||||
"l_puk_mismatch": "PUKs do not match",
|
||||
"l_new_pin_len": "New PIN must be at least {length} characters",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "New password",
|
||||
"s_current_password": "Current password",
|
||||
"s_confirm_password": "Confirm password",
|
||||
"l_password_mismatch": "Passwords do not match",
|
||||
"s_wrong_password": "Wrong password",
|
||||
"s_remove_password": "Remove password",
|
||||
"s_password_removed": "Password removed",
|
||||
|
@ -66,7 +66,7 @@
|
||||
"s_settings": "Paramètres",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "Certificats",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_security_key": null,
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "Aide et à propos",
|
||||
"l_help_and_about_desc": null,
|
||||
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Confirmez le PIN",
|
||||
"s_confirm_puk": "Confirmez le PUK",
|
||||
"s_unblock_pin": "Débloquer le PIN",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Nouveau mot de passe",
|
||||
"s_current_password": "Mot de passe actuel",
|
||||
"s_confirm_password": "Confirmez le mot de passe",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Mauvais mot de passe",
|
||||
"s_remove_password": "Supprimer le mot de passe",
|
||||
"s_password_removed": "Mot de passe supprimé",
|
||||
|
@ -66,7 +66,7 @@
|
||||
"s_settings": "設定",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "証明書",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_security_key": null,
|
||||
"s_slots": null,
|
||||
"s_help_and_about": "ヘルプと概要",
|
||||
"l_help_and_about_desc": null,
|
||||
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "PINの確認",
|
||||
"s_confirm_puk": "PUKの確認",
|
||||
"s_unblock_pin": "ブロックを解除",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "新しいパスワード",
|
||||
"s_current_password": "現在のパスワード",
|
||||
"s_confirm_password": "パスワードを確認",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "間違ったパスワード",
|
||||
"s_remove_password": "パスワードの削除",
|
||||
"s_password_removed": "パスワードが削除されました",
|
||||
|
@ -66,7 +66,7 @@
|
||||
"s_settings": "Ustawienia",
|
||||
"l_settings_desc": null,
|
||||
"s_certificates": "Certyfikaty",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_security_key": null,
|
||||
"s_slots": "Sloty",
|
||||
"s_help_and_about": "Pomoc i informacje",
|
||||
"l_help_and_about_desc": null,
|
||||
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Potwierdź PIN",
|
||||
"s_confirm_puk": "Potwierdź PUK",
|
||||
"s_unblock_pin": "Odblokuj PIN",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Nowe hasło",
|
||||
"s_current_password": "Aktualne hasło",
|
||||
"s_confirm_password": "Potwierdź hasło",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Błędne hasło",
|
||||
"s_remove_password": "Usuń hasło",
|
||||
"s_password_removed": "Hasło zostało usunięte",
|
||||
|
@ -390,7 +390,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.nameField,
|
||||
controller: _accountController,
|
||||
@ -418,7 +418,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
@ -460,7 +460,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
|
@ -41,7 +41,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
String _currentPassword = '';
|
||||
final _currentPasswordController = TextEditingController();
|
||||
final _currentPasswordFocus = FocusNode();
|
||||
String _newPassword = '';
|
||||
String _confirmPassword = '';
|
||||
bool _currentIsWrong = false;
|
||||
@ -49,12 +50,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPasswordController.dispose();
|
||||
_currentPasswordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
FocusUtils.unfocus(context);
|
||||
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.setPassword(_currentPassword, _newPassword);
|
||||
.setPassword(_currentPasswordController.text, _newPassword);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
@ -63,6 +71,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -72,9 +83,10 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isValid = _newPassword.isNotEmpty &&
|
||||
final isValid = !_currentIsWrong &&
|
||||
_newPassword.isNotEmpty &&
|
||||
_newPassword == _confirmPassword &&
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty);
|
||||
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(
|
||||
@ -98,6 +110,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
obscureText: _isObscureCurrent,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.currentPasswordField,
|
||||
controller: _currentPasswordController,
|
||||
focusNode: _currentPasswordFocus,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_current_password,
|
||||
@ -121,21 +135,21 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
key: keys.removePasswordButton,
|
||||
onPressed: _currentPassword.isNotEmpty
|
||||
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||
!_currentIsWrong
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(_currentPassword);
|
||||
.unsetPassword(_currentPasswordController.text);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)(
|
||||
@ -145,6 +159,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection =
|
||||
TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _currentPasswordController
|
||||
.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -193,7 +213,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
tooltip: _isObscureNew
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||
enabled: !widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -206,7 +227,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.confirmPasswordField,
|
||||
obscureText: _isObscureConfirm,
|
||||
@ -227,9 +248,14 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
tooltip: _isObscureConfirm
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled:
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
enabled: (!widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
errorText: _newPassword.length == _confirmPassword.length &&
|
||||
_newPassword != _confirmPassword
|
||||
? l10n.l_password_mismatch
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
@ -242,7 +268,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -443,7 +443,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
Focus.of(context)
|
||||
.focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -193,7 +193,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
_issuer = value.trim();
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextFormField(
|
||||
initialValue: _name,
|
||||
maxLength: nameRemaining,
|
||||
@ -222,7 +222,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -38,6 +38,7 @@ class UnlockForm extends ConsumerStatefulWidget {
|
||||
|
||||
class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
final _passwordController = TextEditingController();
|
||||
final _passwordFocus = FocusNode();
|
||||
bool _remember = false;
|
||||
bool _passwordIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
@ -51,9 +52,11 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
.unlock(_passwordController.text, remember: _remember);
|
||||
if (!mounted) return;
|
||||
if (!success) {
|
||||
_passwordController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _passwordController.text.length);
|
||||
_passwordFocus.requestFocus();
|
||||
setState(() {
|
||||
_passwordIsWrong = true;
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else if (_remember && !remembered) {
|
||||
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
|
||||
@ -79,6 +82,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
child: AppTextField(
|
||||
key: keys.passwordField,
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
@ -106,7 +110,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
_passwordIsWrong = false;
|
||||
}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
),
|
||||
const SizedBox(height: 3.0),
|
||||
Column(
|
||||
@ -143,7 +147,8 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
key: keys.unlockButton,
|
||||
label: Text(l10n.s_unlock),
|
||||
icon: const Icon(Symbols.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty
|
||||
onPressed: _passwordController.text.isNotEmpty &&
|
||||
!_passwordIsWrong
|
||||
? _submit
|
||||
: null,
|
||||
),
|
||||
|
@ -166,7 +166,7 @@ class _ConfigureChalrespDialogState
|
||||
_validateSecret = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
FilterChip(
|
||||
label: Text(l10n.s_require_touch),
|
||||
selected: _requireTouch,
|
||||
|
@ -127,6 +127,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
obscureText: _isObscure,
|
||||
autofocus: true,
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -158,7 +159,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
_validateSecret = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -181,7 +181,7 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
_validatePassword = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -237,7 +237,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validatePublicIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.privateIdField,
|
||||
controller: _privateIdController,
|
||||
@ -274,7 +274,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validatePrivateIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
@ -311,7 +311,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validateSecretFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -35,8 +35,7 @@ abstract class PivStateNotifier extends ApplicationStateNotifier<PivState> {
|
||||
bool storeKey = false,
|
||||
});
|
||||
|
||||
Future<PinVerificationStatus> verifyPin(
|
||||
String pin); //TODO: Maybe return authenticated?
|
||||
Future<PinVerificationStatus> verifyPin(String pin);
|
||||
Future<PinVerificationStatus> changePin(String pin, String newPin);
|
||||
Future<PinVerificationStatus> changePuk(String puk, String newPuk);
|
||||
Future<PinVerificationStatus> unblockPin(String puk, String newPin);
|
||||
|
@ -52,24 +52,26 @@ class ExportIntent extends Intent {
|
||||
const ExportIntent(this.slot);
|
||||
}
|
||||
|
||||
Future<bool> _authenticate(
|
||||
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||
return await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => pivState.protectedKey
|
||||
? PinDialog(devicePath)
|
||||
: AuthenticationDialog(
|
||||
devicePath,
|
||||
pivState,
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> _authIfNeeded(
|
||||
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||
Future<bool> _authIfNeeded(BuildContext context, WidgetRef ref,
|
||||
DevicePath devicePath, PivState pivState) async {
|
||||
if (pivState.needsAuth) {
|
||||
return await _authenticate(context, devicePath, pivState);
|
||||
if (pivState.protectedKey &&
|
||||
pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||
final status = await ref
|
||||
.read(pivStateProvider(devicePath).notifier)
|
||||
.verifyPin(defaultPin);
|
||||
return status.when(success: () => true, failure: (_) => false);
|
||||
}
|
||||
return await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => pivState.protectedKey
|
||||
? PinDialog(devicePath)
|
||||
: AuthenticationDialog(
|
||||
devicePath,
|
||||
pivState,
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -96,21 +98,32 @@ class PivActions extends ConsumerWidget {
|
||||
if (hasFeature(features.slotsGenerate))
|
||||
GenerateIntent:
|
||||
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
||||
if (!pivState.protectedKey &&
|
||||
!await withContext((context) =>
|
||||
_authIfNeeded(context, devicePath, pivState))) {
|
||||
//Verify management key and maybe PIN
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify PIN, unless already done above
|
||||
// TODO: Avoid asking for PIN if not needed?
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(devicePath))) ??
|
||||
false;
|
||||
if (!pivState.protectedKey) {
|
||||
bool verified;
|
||||
if (pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||
final status = await ref
|
||||
.read(pivStateProvider(devicePath).notifier)
|
||||
.verifyPin(defaultPin);
|
||||
verified =
|
||||
status.when(success: () => true, failure: (_) => false);
|
||||
} else {
|
||||
verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(devicePath))) ??
|
||||
false;
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
return false;
|
||||
if (!verified) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await withContext((context) async {
|
||||
@ -158,8 +171,8 @@ class PivActions extends ConsumerWidget {
|
||||
}),
|
||||
if (hasFeature(features.slotsImport))
|
||||
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
||||
if (!await withContext(
|
||||
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -243,8 +256,8 @@ class PivActions extends ConsumerWidget {
|
||||
if (hasFeature(features.slotsDelete))
|
||||
DeleteIntent<PivSlot>:
|
||||
CallbackAction<DeleteIntent<PivSlot>>(onInvoke: (intent) async {
|
||||
if (!await withContext(
|
||||
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
bool _keyIsWrong = false;
|
||||
bool _keyFormatInvalid = false;
|
||||
final _keyController = TextEditingController();
|
||||
final _keyFocus = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyController.dispose();
|
||||
_keyFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -65,7 +67,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.unlockButton,
|
||||
onPressed: _keyController.text.length == keyLen
|
||||
onPressed: !_keyIsWrong && _keyController.text.length == keyLen
|
||||
? () async {
|
||||
if (keyFormatInvalid) {
|
||||
setState(() {
|
||||
@ -81,6 +83,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
if (status) {
|
||||
navigator.pop(true);
|
||||
} else {
|
||||
_keyController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _keyController.text.length);
|
||||
_keyFocus.requestFocus();
|
||||
setState(() {
|
||||
_keyIsWrong = true;
|
||||
});
|
||||
@ -88,6 +94,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
} on CancellationException catch (_) {
|
||||
navigator.pop(false);
|
||||
} catch (_) {
|
||||
_keyController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _keyController.text.length);
|
||||
_keyFocus.requestFocus();
|
||||
// TODO: More error cases
|
||||
setState(() {
|
||||
_keyIsWrong = true;
|
||||
@ -109,6 +119,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
autofocus: true,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _keyController,
|
||||
focusNode: _keyFocus,
|
||||
readOnly: _defaultKeyUsed,
|
||||
maxLength: !_defaultKeyUsed ? keyLen : null,
|
||||
decoration: AppInputDecoration(
|
||||
@ -149,7 +160,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
_keyFormatInvalid = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -174,7 +174,7 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
||||
_subject = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Text(
|
||||
l10n.rfc4514_examples,
|
||||
style: subtitleStyle,
|
||||
|
@ -162,7 +162,7 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) => _examine(),
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -60,7 +60,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
title: l10n.s_pin,
|
||||
title: l10n.s_change_pin,
|
||||
subtitle: pinBlocked
|
||||
? (pukAttempts != 0
|
||||
? l10n.l_piv_pin_blocked
|
||||
@ -88,7 +88,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
feature: features.actionsPuk,
|
||||
title: l10n.s_puk,
|
||||
title: l10n.s_change_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? (pukAttempts == 0
|
||||
? l10n.l_piv_pin_puk_blocked
|
||||
|
@ -30,6 +30,7 @@ import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
import '../../widgets/choice_filter_chip.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -48,6 +49,7 @@ class ManageKeyDialog extends ConsumerStatefulWidget {
|
||||
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
late bool _hasMetadata;
|
||||
late bool _defaultKeyUsed;
|
||||
late bool _defaultPinUsed;
|
||||
late bool _usesStoredKey;
|
||||
late bool _storeKey;
|
||||
bool _currentIsWrong = false;
|
||||
@ -56,6 +58,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
int _attemptsRemaining = -1;
|
||||
late ManagementKeyType _keyType;
|
||||
final _currentController = TextEditingController();
|
||||
final _currentFocus = FocusNode();
|
||||
final _keyController = TextEditingController();
|
||||
bool _isObscure = true;
|
||||
|
||||
@ -68,9 +71,13 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
defaultManagementKeyType;
|
||||
_defaultKeyUsed =
|
||||
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
|
||||
_defaultPinUsed =
|
||||
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
||||
_usesStoredKey = widget.pivState.protectedKey;
|
||||
if (!_usesStoredKey && _defaultKeyUsed) {
|
||||
_currentController.text = defaultManagementKey;
|
||||
} else if (_usesStoredKey && _defaultPinUsed) {
|
||||
_currentController.text = defaultPin;
|
||||
}
|
||||
_storeKey = _usesStoredKey;
|
||||
}
|
||||
@ -79,16 +86,18 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
void dispose() {
|
||||
_keyController.dispose();
|
||||
_currentController.dispose();
|
||||
_currentFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
|
||||
final newInvalidFormat = Format.hex.isValid(_keyController.text);
|
||||
if (!currentInvalidFormat || !newInvalidFormat) {
|
||||
final currentValidFormat =
|
||||
_usesStoredKey || Format.hex.isValid(_currentController.text);
|
||||
final newValidFormat = Format.hex.isValid(_keyController.text);
|
||||
if (!currentValidFormat || !newValidFormat) {
|
||||
setState(() {
|
||||
_currentInvalidFormat = !currentInvalidFormat;
|
||||
_newInvalidFormat = !newInvalidFormat;
|
||||
_currentInvalidFormat = !currentValidFormat;
|
||||
_newInvalidFormat = !newValidFormat;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -98,6 +107,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
final status = (await notifier.verifyPin(_currentController.text)).when(
|
||||
success: () => true,
|
||||
failure: (attemptsRemaining) {
|
||||
_currentController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||
_currentFocus.requestFocus();
|
||||
setState(() {
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_currentIsWrong = true;
|
||||
@ -110,6 +122,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
}
|
||||
} else {
|
||||
if (!await notifier.authenticate(_currentController.text)) {
|
||||
_currentController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||
_currentFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -118,15 +133,19 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
}
|
||||
|
||||
if (_storeKey && !_usesStoredKey) {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(widget.path))) ??
|
||||
false;
|
||||
if (_defaultPinUsed) {
|
||||
await notifier.verifyPin(defaultPin);
|
||||
} else {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(widget.path))) ??
|
||||
false;
|
||||
|
||||
if (!verified) {
|
||||
return;
|
||||
if (!verified) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,9 +166,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
widget.pivState.metadata?.managementKeyMetadata.keyType ??
|
||||
defaultManagementKeyType;
|
||||
final hexLength = _keyType.keyLength * 2;
|
||||
final protected = widget.pivState.protectedKey;
|
||||
final currentKeyOrPin = _currentController.text;
|
||||
final currentLenOk = protected
|
||||
final currentLenOk = _usesStoredKey
|
||||
? currentKeyOrPin.length >= 4
|
||||
: currentKeyOrPin.length == currentType.keyLength * 2;
|
||||
final newLenOk = _keyController.text.length == hexLength;
|
||||
@ -158,7 +176,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
title: Text(l10n.l_change_management_key),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: currentLenOk && newLenOk ? _submit : null,
|
||||
onPressed:
|
||||
!_currentIsWrong && currentLenOk && newLenOk ? _submit : null,
|
||||
key: keys.saveButton,
|
||||
child: Text(l10n.s_save),
|
||||
)
|
||||
@ -169,23 +188,25 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.p_change_management_key_desc),
|
||||
if (protected)
|
||||
if (_usesStoredKey)
|
||||
AppTextField(
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.pinPukField,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_currentController.text),
|
||||
controller: _currentController,
|
||||
focusNode: _currentFocus,
|
||||
readOnly: _defaultPinUsed,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
helperText: _defaultPinUsed ? l10n.l_default_pin_used : null,
|
||||
errorText: _currentIsWrong
|
||||
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||
: _currentInvalidFormat
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.hex.allowedCharacters)
|
||||
: null,
|
||||
: null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
suffixIcon: IconButton(
|
||||
@ -206,13 +227,14 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_currentInvalidFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!protected)
|
||||
).init(),
|
||||
if (!_usesStoredKey)
|
||||
AppTextFormField(
|
||||
key: keys.managementKeyField,
|
||||
autofocus: !_defaultKeyUsed,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _currentController,
|
||||
focusNode: _currentFocus,
|
||||
readOnly: _defaultKeyUsed,
|
||||
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
||||
decoration: AppInputDecoration(
|
||||
@ -251,7 +273,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.newPinPukField,
|
||||
autofocus: _defaultKeyUsed,
|
||||
@ -299,7 +321,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -24,6 +24,7 @@ import '../../app/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -44,20 +45,25 @@ class ManagePinPukDialog extends ConsumerStatefulWidget {
|
||||
|
||||
class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
final _currentPinController = TextEditingController();
|
||||
final _currentPinFocus = FocusNode();
|
||||
String _newPin = '';
|
||||
String _confirmPin = '';
|
||||
bool _pinIsBlocked = false;
|
||||
bool _currentIsWrong = false;
|
||||
int _attemptsRemaining = -1;
|
||||
bool _isObscureCurrent = true;
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
late bool _defaultPinUsed;
|
||||
late bool _defaultPukUsed;
|
||||
late final bool _defaultPinUsed;
|
||||
late final bool _defaultPukUsed;
|
||||
late final int _minPinLen;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Old YubiKeys allowed a 4 digit PIN
|
||||
_minPinLen = widget.pivState.version.isAtLeast(4, 3, 1) ? 6 : 4;
|
||||
_defaultPinUsed =
|
||||
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
||||
_defaultPukUsed =
|
||||
@ -73,6 +79,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPinController.dispose();
|
||||
_currentPinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -98,11 +105,16 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_ => l10n.s_pin_set,
|
||||
});
|
||||
}, failure: (attemptsRemaining) {
|
||||
_currentPinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||
_currentPinFocus.requestFocus();
|
||||
setState(() {
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_currentIsWrong = true;
|
||||
if (_attemptsRemaining == 0) {
|
||||
_pinIsBlocked = true;
|
||||
}
|
||||
});
|
||||
_currentPinController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@ -110,8 +122,12 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final currentPin = _currentPinController.text;
|
||||
final isValid =
|
||||
_newPin.isNotEmpty && _newPin == _confirmPin && currentPin.isNotEmpty;
|
||||
final currentPinLen = byteLength(currentPin);
|
||||
final newPinLen = byteLength(_newPin);
|
||||
final isValid = !_currentIsWrong &&
|
||||
_newPin.isNotEmpty &&
|
||||
_newPin == _confirmPin &&
|
||||
currentPin.isNotEmpty;
|
||||
|
||||
final titleText = switch (widget.target) {
|
||||
ManageTarget.pin => l10n.s_change_pin,
|
||||
@ -138,7 +154,6 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//TODO fix string
|
||||
Text(widget.target == ManageTarget.pin
|
||||
? l10n.p_enter_current_pin_or_reset
|
||||
: l10n.p_enter_current_puk_or_reset),
|
||||
@ -146,10 +161,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
autofocus: !(showDefaultPinUsed || showDefaultPukUsed),
|
||||
obscureText: _isObscureCurrent,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(currentPin),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.pinPukField,
|
||||
readOnly: showDefaultPinUsed || showDefaultPukUsed,
|
||||
controller: _currentPinController,
|
||||
focusNode: _currentPinFocus,
|
||||
enabled: !_pinIsBlocked,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: showDefaultPinUsed
|
||||
@ -160,13 +179,17 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
labelText: widget.target == ManageTarget.pin
|
||||
? l10n.s_current_pin
|
||||
: l10n.s_current_puk,
|
||||
errorText: _currentIsWrong
|
||||
errorText: _pinIsBlocked
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n
|
||||
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||
: l10n
|
||||
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
|
||||
: null,
|
||||
? l10n.l_piv_pin_blocked
|
||||
: l10n.l_piv_pin_puk_blocked)
|
||||
: (_currentIsWrong
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n.l_wrong_pin_attempts_remaining(
|
||||
_attemptsRemaining)
|
||||
: l10n.l_wrong_puk_attempts_remaining(
|
||||
_attemptsRemaining))
|
||||
: null),
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.password),
|
||||
suffixIcon: IconButton(
|
||||
@ -189,7 +212,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Text(l10n.p_enter_new_piv_pin_puk(
|
||||
widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)),
|
||||
AppTextField(
|
||||
@ -197,6 +220,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
autofocus: showDefaultPinUsed || showDefaultPukUsed,
|
||||
obscureText: _isObscureNew,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_newPin),
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -217,8 +242,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||
: (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||
),
|
||||
// Old YubiKeys allowed a 4 digit PIN
|
||||
enabled: currentPin.length >= 4,
|
||||
enabled: currentPinLen >= _minPinLen,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -231,11 +255,13 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.confirmPinPukField,
|
||||
obscureText: _isObscureConfirm,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_confirmPin),
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -256,7 +282,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||
: (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||
),
|
||||
enabled: currentPin.length >= 4 && _newPin.length >= 6,
|
||||
enabled: currentPinLen >= _minPinLen && newPinLen >= 6,
|
||||
errorText:
|
||||
newPinLen == _confirmPin.length && _newPin != _confirmPin
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n.l_pin_mismatch
|
||||
: l10n.l_puk_mismatch)
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
@ -269,7 +302,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -24,6 +24,7 @@ import '../../exception/cancellation_exception.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../state.dart';
|
||||
|
||||
@ -37,6 +38,7 @@ class PinDialog extends ConsumerStatefulWidget {
|
||||
|
||||
class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinFocus = FocusNode();
|
||||
bool _pinIsWrong = false;
|
||||
int _attemptsRemaining = -1;
|
||||
bool _isObscure = true;
|
||||
@ -44,6 +46,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_pinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -58,8 +61,10 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
navigator.pop(true);
|
||||
},
|
||||
failure: (attemptsRemaining) {
|
||||
_pinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||
_pinFocus.requestFocus();
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_pinIsWrong = true;
|
||||
});
|
||||
@ -73,12 +78,15 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final version = ref.watch(pivStateProvider(widget.devicePath)).valueOrNull;
|
||||
final minPinLen = version?.version.isAtLeast(4, 3, 1) == true ? 6 : 4;
|
||||
final currentPinLen = byteLength(_pinController.text);
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_pin_required),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.unlockButton,
|
||||
onPressed: _pinController.text.length >= 4 ? _submit : null,
|
||||
onPressed: currentPinLen >= minPinLen ? _submit : null,
|
||||
child: Text(l10n.s_unlock),
|
||||
),
|
||||
],
|
||||
@ -92,9 +100,12 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_pinController.text),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.managementKeyField,
|
||||
controller: _pinController,
|
||||
focusNode: _pinFocus,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
@ -121,7 +132,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -226,7 +226,7 @@ class _CertificateListItem extends ConsumerWidget {
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: colorScheme.onSecondary,
|
||||
backgroundColor: colorScheme.secondary,
|
||||
child: const Icon(Symbols.approval),
|
||||
child: const Icon(Symbols.badge),
|
||||
),
|
||||
title: slot.getDisplayName(l10n),
|
||||
subtitle: certInfo != null
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_input_decoration.dart';
|
||||
@ -86,4 +85,13 @@ class AppTextField extends TextField {
|
||||
super.spellCheckConfiguration,
|
||||
super.magnifierConfiguration,
|
||||
}) : super(decoration: decoration);
|
||||
|
||||
Widget init() => Builder(
|
||||
builder: (context) => DefaultSelectionStyle(
|
||||
selectionColor: decoration?.errorText != null
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
child: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'app_input_decoration.dart';
|
||||
|
||||
/// TextFormField without autocorrect and suggestions
|
||||
class AppTextFormField extends TextFormField {
|
||||
final AppInputDecoration? decoration;
|
||||
AppTextFormField({
|
||||
// default settings to turn off autocorrect
|
||||
super.autocorrect = false,
|
||||
@ -30,7 +31,7 @@ class AppTextFormField extends TextFormField {
|
||||
super.controller,
|
||||
super.initialValue,
|
||||
super.focusNode,
|
||||
AppInputDecoration? decoration,
|
||||
this.decoration,
|
||||
super.textCapitalization,
|
||||
super.textInputAction,
|
||||
super.style,
|
||||
@ -90,4 +91,13 @@ class AppTextFormField extends TextFormField {
|
||||
super.scribbleEnabled,
|
||||
super.canRequestFocus,
|
||||
}) : super(decoration: decoration);
|
||||
|
||||
Widget init() => Builder(
|
||||
builder: (context) => DefaultSelectionStyle(
|
||||
selectionColor: decoration?.errorText != null
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
child: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
@ -30,6 +31,8 @@ class _AppLinter extends PluginBase {
|
||||
discouraged: 'TextFormField',
|
||||
recommended: 'AppTextFormField',
|
||||
),
|
||||
const CallInitAfterCreation(className: 'AppTextField'),
|
||||
const CallInitAfterCreation(className: 'AppTextFormField'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -59,3 +62,35 @@ class UseRecommendedWidget extends DartLintRule {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CallInitAfterCreation extends DartLintRule {
|
||||
final String className;
|
||||
|
||||
const CallInitAfterCreation({required this.className})
|
||||
: super(
|
||||
code: const LintCode(
|
||||
name: 'call_init_after_creation',
|
||||
problemMessage: 'Call init() after creation',
|
||||
));
|
||||
|
||||
@override
|
||||
void run(
|
||||
CustomLintResolver resolver,
|
||||
ErrorReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
context.registry.addInstanceCreationExpression((node) {
|
||||
if (node.constructorName.toString() == className) {
|
||||
final dot = node.endToken.next;
|
||||
final next = dot?.next;
|
||||
if (dot?.type == TokenType.PERIOD) {
|
||||
if (next?.type == TokenType.IDENTIFIER &&
|
||||
next?.toString() == 'init') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
reporter.reportErrorForNode(code, node.constructorName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user