mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-26 10:33:15 +03:00
Merge PR #1302
This commit is contained in:
commit
624f4aae4f
@ -413,7 +413,9 @@ class ConnectionNode(RpcNode):
|
|||||||
or ( # SmartCardConnection can be used over NFC, or on 5.3 and later.
|
or ( # SmartCardConnection can be used over NFC, or on 5.3 and later.
|
||||||
isinstance(self._connection, SmartCardConnection)
|
isinstance(self._connection, SmartCardConnection)
|
||||||
and (
|
and (
|
||||||
self._transport == TRANSPORT.NFC or self._info.version >= (5, 3, 0)
|
self._transport == TRANSPORT.NFC
|
||||||
|
or self._info.version >= (5, 3, 0)
|
||||||
|
or self._info.version[0] == 3
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
|
|
||||||
from .base import RpcNode, action, child
|
from .base import RpcNode, action, child
|
||||||
|
|
||||||
from yubikit.core import NotSupportedError
|
from yubikit.core import NotSupportedError, CommandError
|
||||||
|
from yubikit.core.otp import modhex_encode, modhex_decode
|
||||||
from yubikit.yubiotp import (
|
from yubikit.yubiotp import (
|
||||||
YubiOtpSession,
|
YubiOtpSession,
|
||||||
SLOT,
|
SLOT,
|
||||||
@ -25,7 +26,17 @@ from yubikit.yubiotp import (
|
|||||||
YubiOtpSlotConfiguration,
|
YubiOtpSlotConfiguration,
|
||||||
StaticTicketSlotConfiguration,
|
StaticTicketSlotConfiguration,
|
||||||
)
|
)
|
||||||
|
from ykman.otp import generate_static_pw, format_csv
|
||||||
|
from yubikit.oath import parse_b32_key
|
||||||
|
from ykman.scancodes import KEYBOARD_LAYOUT, encode
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
import struct
|
||||||
|
|
||||||
|
_FAIL_MSG = (
|
||||||
|
"Failed to write to the YubiKey. Make sure the device does not "
|
||||||
|
"have restricted access"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class YubiOtpNode(RpcNode):
|
class YubiOtpNode(RpcNode):
|
||||||
@ -65,6 +76,29 @@ class YubiOtpNode(RpcNode):
|
|||||||
def two(self):
|
def two(self):
|
||||||
return SlotNode(self.session, SLOT.TWO)
|
return SlotNode(self.session, SLOT.TWO)
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def serial_modhex(self, params, event, signal):
|
||||||
|
serial = params["serial"]
|
||||||
|
return dict(encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)))
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def generate_static(self, params, event, signal):
|
||||||
|
layout, length = params["layout"], int(params["length"])
|
||||||
|
return dict(password=generate_static_pw(length, KEYBOARD_LAYOUT[layout]))
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def keyboard_layouts(self, params, event, signal):
|
||||||
|
return {layout.name: [sc for sc in layout.value] for layout in KEYBOARD_LAYOUT}
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def format_yubiotp_csv(self, params, even, signal):
|
||||||
|
serial = params["serial"]
|
||||||
|
public_id = modhex_decode(params["public_id"])
|
||||||
|
private_id = bytes.fromhex(params["private_id"])
|
||||||
|
key = bytes.fromhex(params["key"])
|
||||||
|
|
||||||
|
return dict(csv=format_csv(serial, public_id, private_id, key))
|
||||||
|
|
||||||
|
|
||||||
_CONFIG_TYPES = dict(
|
_CONFIG_TYPES = dict(
|
||||||
hmac_sha1=HmacSha1SlotConfiguration,
|
hmac_sha1=HmacSha1SlotConfiguration,
|
||||||
@ -113,7 +147,10 @@ class SlotNode(RpcNode):
|
|||||||
|
|
||||||
@action(condition=lambda self: self._maybe_configured(self.slot))
|
@action(condition=lambda self: self._maybe_configured(self.slot))
|
||||||
def delete(self, params, event, signal):
|
def delete(self, params, event, signal):
|
||||||
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
|
try:
|
||||||
|
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
|
||||||
|
except CommandError:
|
||||||
|
raise ValueError(_FAIL_MSG)
|
||||||
|
|
||||||
@action(condition=lambda self: self._can_calculate(self.slot))
|
@action(condition=lambda self: self._can_calculate(self.slot))
|
||||||
def calculate(self, params, event, signal):
|
def calculate(self, params, event, signal):
|
||||||
@ -121,7 +158,7 @@ class SlotNode(RpcNode):
|
|||||||
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
|
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
|
||||||
return dict(response=response)
|
return dict(response=response)
|
||||||
|
|
||||||
def _apply_config(self, config, params):
|
def _apply_options(self, config, options):
|
||||||
for option in (
|
for option in (
|
||||||
"serial_api_visible",
|
"serial_api_visible",
|
||||||
"serial_usb_visible",
|
"serial_usb_visible",
|
||||||
@ -140,39 +177,61 @@ class SlotNode(RpcNode):
|
|||||||
"short_ticket",
|
"short_ticket",
|
||||||
"manual_update",
|
"manual_update",
|
||||||
):
|
):
|
||||||
if option in params:
|
if option in options:
|
||||||
getattr(config, option)(params.pop(option))
|
getattr(config, option)(options.pop(option))
|
||||||
|
|
||||||
for option in ("tabs", "delay", "pacing", "strong_password"):
|
for option in ("tabs", "delay", "pacing", "strong_password"):
|
||||||
if option in params:
|
if option in options:
|
||||||
getattr(config, option)(*params.pop(option))
|
getattr(config, option)(*options.pop(option))
|
||||||
|
|
||||||
if "token_id" in params:
|
if "token_id" in options:
|
||||||
token_id, *args = params.pop("token_id")
|
token_id, *args = options.pop("token_id")
|
||||||
config.token_id(bytes.fromhex(token_id), *args)
|
config.token_id(bytes.fromhex(token_id), *args)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _get_config(self, type, **kwargs):
|
||||||
|
config = None
|
||||||
|
|
||||||
|
if type in _CONFIG_TYPES:
|
||||||
|
if type == "hmac_sha1":
|
||||||
|
config = _CONFIG_TYPES[type](bytes.fromhex(kwargs["key"]))
|
||||||
|
elif type == "hotp":
|
||||||
|
config = _CONFIG_TYPES[type](parse_b32_key(kwargs["key"]))
|
||||||
|
elif type == "static_password":
|
||||||
|
config = _CONFIG_TYPES[type](
|
||||||
|
encode(
|
||||||
|
kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif type == "yubiotp":
|
||||||
|
config = _CONFIG_TYPES[type](
|
||||||
|
fixed=modhex_decode(kwargs["public_id"]),
|
||||||
|
uid=bytes.fromhex(kwargs["private_id"]),
|
||||||
|
key=bytes.fromhex(kwargs["key"]),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("No supported configuration type provided.")
|
||||||
|
return config
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def put(self, params, event, signal):
|
def put(self, params, event, signal):
|
||||||
config = None
|
type = params.pop("type")
|
||||||
for key in _CONFIG_TYPES:
|
options = params.pop("options", {})
|
||||||
if key in params:
|
args = params
|
||||||
if config is not None:
|
|
||||||
raise ValueError("Only one configuration type can be provided.")
|
config = self._get_config(type, **args)
|
||||||
config = _CONFIG_TYPES[key](
|
self._apply_options(config, options)
|
||||||
*(bytes.fromhex(arg) for arg in params.pop(key))
|
try:
|
||||||
)
|
self.session.put_configuration(
|
||||||
if config is None:
|
self.slot,
|
||||||
raise ValueError("No supported configuration type provided.")
|
config,
|
||||||
self._apply_config(config, params)
|
params.pop("acc_code", None),
|
||||||
self.session.put_configuration(
|
params.pop("cur_acc_code", None),
|
||||||
self.slot,
|
)
|
||||||
config,
|
return dict()
|
||||||
params.pop("acc_code", None),
|
except CommandError:
|
||||||
params.pop("cur_acc_code", None),
|
raise ValueError(_FAIL_MSG)
|
||||||
)
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
condition=lambda self: self._state.version >= (2, 2, 0)
|
condition=lambda self: self._state.version >= (2, 2, 0)
|
||||||
@ -180,7 +239,7 @@ class SlotNode(RpcNode):
|
|||||||
)
|
)
|
||||||
def update(self, params, event, signal):
|
def update(self, params, event, signal):
|
||||||
config = UpdateConfiguration()
|
config = UpdateConfiguration()
|
||||||
self._apply_config(config, params)
|
self._apply_options(config, params)
|
||||||
self.session.update_configuration(
|
self.session.update_configuration(
|
||||||
self.slot,
|
self.slot,
|
||||||
config,
|
config,
|
||||||
|
34
integration_test/otp_test.dart
Normal file
34
integration_test/otp_test.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Tags(['android', 'desktop', 'oath'])
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:yubico_authenticator/app/views/keys.dart';
|
||||||
|
|
||||||
|
import 'utils/test_util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
||||||
|
|
||||||
|
group('OTP UI tests', () {
|
||||||
|
appTest('OTP menu items exist', (WidgetTester tester) async {
|
||||||
|
await tester.tap(find.byKey(otpAppDrawer));
|
||||||
|
await tester.shortWait();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -158,7 +158,7 @@ void main() {
|
|||||||
const shortmanagementkey =
|
const shortmanagementkey =
|
||||||
'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc';
|
'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc';
|
||||||
|
|
||||||
appTest('Bad managementkey key', (WidgetTester tester) async {
|
appTest('Out of bounds managementkey key', (WidgetTester tester) async {
|
||||||
await tester.configurePiv();
|
await tester.configurePiv();
|
||||||
await tester.shortWait();
|
await tester.shortWait();
|
||||||
await tester.tap(find.byKey(manageManagementKeyAction).hitTestable());
|
await tester.tap(find.byKey(manageManagementKeyAction).hitTestable());
|
||||||
@ -169,12 +169,22 @@ void main() {
|
|||||||
await tester.longWait();
|
await tester.longWait();
|
||||||
await tester.tap(find.byKey(saveButton).hitTestable());
|
await tester.tap(find.byKey(saveButton).hitTestable());
|
||||||
await tester.longWait();
|
await tester.longWait();
|
||||||
|
expect(tester.isTextButtonEnabled(saveButton), true);
|
||||||
|
// TODO assert that errorText and errorIcon are shown
|
||||||
|
});
|
||||||
|
|
||||||
|
appTest('Short managementkey key', (WidgetTester tester) async {
|
||||||
|
await tester.configurePiv();
|
||||||
|
await tester.shortWait();
|
||||||
|
await tester.tap(find.byKey(manageManagementKeyAction).hitTestable());
|
||||||
|
await tester.longWait();
|
||||||
// testing too short management key does not work
|
// testing too short management key does not work
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.byKey(newPinPukField).hitTestable(), shortmanagementkey);
|
find.byKey(newPinPukField).hitTestable(), shortmanagementkey);
|
||||||
await tester.longWait();
|
await tester.longWait();
|
||||||
expect(tester.isTextButtonEnabled(saveButton), false);
|
expect(tester.isTextButtonEnabled(saveButton), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
appTest('Change managementkey key', (WidgetTester tester) async {
|
appTest('Change managementkey key', (WidgetTester tester) async {
|
||||||
await tester.configurePiv();
|
await tester.configurePiv();
|
||||||
await tester.shortWait();
|
await tester.shortWait();
|
||||||
|
@ -54,7 +54,8 @@ enum Application {
|
|||||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||||
Application.oath => l10n.s_authenticator,
|
Application.oath => l10n.s_authenticator,
|
||||||
Application.fido => l10n.s_webauthn,
|
Application.fido => l10n.s_webauthn,
|
||||||
Application.piv => l10n.s_piv,
|
Application.piv => l10n.s_certificates,
|
||||||
|
Application.otp => l10n.s_slots,
|
||||||
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
|
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import '../../core/state.dart';
|
|||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../fido/views/fido_screen.dart';
|
import '../../fido/views/fido_screen.dart';
|
||||||
import '../../oath/views/oath_screen.dart';
|
import '../../oath/views/oath_screen.dart';
|
||||||
|
import '../../otp/views/otp_screen.dart';
|
||||||
import '../../piv/views/piv_screen.dart';
|
import '../../piv/views/piv_screen.dart';
|
||||||
import '../../widgets/custom_icons.dart';
|
import '../../widgets/custom_icons.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -150,6 +151,7 @@ class MainPage extends ConsumerWidget {
|
|||||||
Application.oath => OathScreen(data.node.path),
|
Application.oath => OathScreen(data.node.path),
|
||||||
Application.fido => FidoScreen(data),
|
Application.fido => FidoScreen(data),
|
||||||
Application.piv => PivScreen(data.node.path),
|
Application.piv => PivScreen(data.node.path),
|
||||||
|
Application.otp => OtpScreen(data.node.path),
|
||||||
_ => MessagePage(
|
_ => MessagePage(
|
||||||
header: l10n.s_app_not_supported,
|
header: l10n.s_app_not_supported,
|
||||||
message: l10n.l_app_not_supported_desc,
|
message: l10n.l_app_not_supported_desc,
|
||||||
|
@ -89,7 +89,7 @@ extension on Application {
|
|||||||
IconData get _icon => switch (this) {
|
IconData get _icon => switch (this) {
|
||||||
Application.oath => Icons.supervisor_account_outlined,
|
Application.oath => Icons.supervisor_account_outlined,
|
||||||
Application.fido => Icons.security_outlined,
|
Application.fido => Icons.security_outlined,
|
||||||
Application.otp => Icons.password_outlined,
|
Application.otp => Icons.touch_app_outlined,
|
||||||
Application.piv => Icons.approval_outlined,
|
Application.piv => Icons.approval_outlined,
|
||||||
Application.management => Icons.construction_outlined,
|
Application.management => Icons.construction_outlined,
|
||||||
Application.openpgp => Icons.key_outlined,
|
Application.openpgp => Icons.key_outlined,
|
||||||
@ -99,7 +99,7 @@ extension on Application {
|
|||||||
IconData get _filledIcon => switch (this) {
|
IconData get _filledIcon => switch (this) {
|
||||||
Application.oath => Icons.supervisor_account,
|
Application.oath => Icons.supervisor_account,
|
||||||
Application.fido => Icons.security,
|
Application.fido => Icons.security,
|
||||||
Application.otp => Icons.password,
|
Application.otp => Icons.touch_app,
|
||||||
Application.piv => Icons.approval,
|
Application.piv => Icons.approval,
|
||||||
Application.management => Icons.construction,
|
Application.management => Icons.construction,
|
||||||
Application.openpgp => Icons.key,
|
Application.openpgp => Icons.key,
|
||||||
|
@ -155,3 +155,18 @@ class Version with _$Version implements Comparable<Version> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');
|
final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
enum Format {
|
||||||
|
base32('a-z2-7'),
|
||||||
|
hex('abcdef0123456789'),
|
||||||
|
modhex('cbdefghijklnrtuv');
|
||||||
|
|
||||||
|
final String allowedCharacters;
|
||||||
|
|
||||||
|
const Format(this.allowedCharacters);
|
||||||
|
|
||||||
|
bool isValid(String input) {
|
||||||
|
return RegExp('^[$allowedCharacters]+\$', caseSensitive: false)
|
||||||
|
.hasMatch(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -42,12 +42,14 @@ import '../core/state.dart';
|
|||||||
import '../fido/state.dart';
|
import '../fido/state.dart';
|
||||||
import '../management/state.dart';
|
import '../management/state.dart';
|
||||||
import '../oath/state.dart';
|
import '../oath/state.dart';
|
||||||
|
import '../otp/state.dart';
|
||||||
import '../piv/state.dart';
|
import '../piv/state.dart';
|
||||||
import '../version.dart';
|
import '../version.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
import 'fido/state.dart';
|
import 'fido/state.dart';
|
||||||
import 'management/state.dart';
|
import 'management/state.dart';
|
||||||
import 'oath/state.dart';
|
import 'oath/state.dart';
|
||||||
|
import 'otp/state.dart';
|
||||||
import 'piv/state.dart';
|
import 'piv/state.dart';
|
||||||
import 'qr_scanner.dart';
|
import 'qr_scanner.dart';
|
||||||
import 'rpc.dart';
|
import 'rpc.dart';
|
||||||
@ -189,6 +191,7 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
Application.fido,
|
Application.fido,
|
||||||
Application.piv,
|
Application.piv,
|
||||||
Application.management,
|
Application.management,
|
||||||
|
Application.otp
|
||||||
])),
|
])),
|
||||||
prefProvider.overrideWithValue(prefs),
|
prefProvider.overrideWithValue(prefs),
|
||||||
rpcProvider.overrideWith((_) => rpcFuture),
|
rpcProvider.overrideWith((_) => rpcFuture),
|
||||||
@ -226,6 +229,8 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
// PIV
|
// PIV
|
||||||
pivStateProvider.overrideWithProvider(desktopPivState.call),
|
pivStateProvider.overrideWithProvider(desktopPivState.call),
|
||||||
pivSlotsProvider.overrideWithProvider(desktopPivSlots.call),
|
pivSlotsProvider.overrideWithProvider(desktopPivSlots.call),
|
||||||
|
// OTP
|
||||||
|
otpStateProvider.overrideWithProvider(desktopOtpState.call)
|
||||||
],
|
],
|
||||||
child: YubicoAuthenticatorApp(
|
child: YubicoAuthenticatorApp(
|
||||||
page: Consumer(
|
page: Consumer(
|
||||||
|
132
lib/desktop/otp/state.dart
Normal file
132
lib/desktop/otp/state.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
import '../../otp/models.dart';
|
||||||
|
import '../../otp/state.dart';
|
||||||
|
import '../rpc.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
|
||||||
|
final _log = Logger('desktop.otp.state');
|
||||||
|
|
||||||
|
final _sessionProvider =
|
||||||
|
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
||||||
|
(ref, devicePath) =>
|
||||||
|
RpcNodeSession(ref.watch(rpcProvider).requireValue, devicePath, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
final desktopOtpState = AsyncNotifierProvider.autoDispose
|
||||||
|
.family<OtpStateNotifier, OtpState, DevicePath>(
|
||||||
|
_DesktopOtpStateNotifier.new);
|
||||||
|
|
||||||
|
class _DesktopOtpStateNotifier extends OtpStateNotifier {
|
||||||
|
late RpcNodeSession _session;
|
||||||
|
List<String> _subpath = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<OtpState> build(DevicePath devicePath) async {
|
||||||
|
_session = ref.watch(_sessionProvider(devicePath));
|
||||||
|
_session.setErrorHandler('state-reset', (_) async {
|
||||||
|
ref.invalidate(_sessionProvider(devicePath));
|
||||||
|
});
|
||||||
|
ref.onDispose(() {
|
||||||
|
_session.unsetErrorHandler('state-reset');
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await _session.command('get');
|
||||||
|
final interfaces = (result['children'] as Map).keys.toSet();
|
||||||
|
|
||||||
|
// Will try to connect over ccid first
|
||||||
|
for (final iface in [UsbInterface.otp, UsbInterface.ccid]) {
|
||||||
|
if (interfaces.contains(iface.name)) {
|
||||||
|
final path = [iface.name, 'yubiotp'];
|
||||||
|
try {
|
||||||
|
final otpStateResult = await _session.command('get', target: path);
|
||||||
|
_subpath = path;
|
||||||
|
_log.debug('Using transport $iface for yubiotp');
|
||||||
|
_log.debug('application status', jsonEncode(result));
|
||||||
|
return OtpState.fromJson(otpStateResult['data']);
|
||||||
|
} catch (e) {
|
||||||
|
_log.warning('Failed connecting to yubiotp via $iface');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw 'Failed connecting over ${UsbInterface.ccid.name} and ${UsbInterface.otp.name}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> swapSlots() async {
|
||||||
|
await _session.command('swap', target: _subpath);
|
||||||
|
ref.invalidate(_sessionProvider(_session.devicePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> generateStaticPassword(int length, String layout) async {
|
||||||
|
final result = await _session.command('generate_static',
|
||||||
|
target: _subpath, params: {'length': length, 'layout': layout});
|
||||||
|
return result['password'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> modhexEncodeSerial(int serial) async {
|
||||||
|
final result = await _session
|
||||||
|
.command('serial_modhex', target: _subpath, params: {'serial': serial});
|
||||||
|
return result['encoded'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> getKeyboardLayouts() async {
|
||||||
|
final result = await _session.command('keyboard_layouts', target: _subpath);
|
||||||
|
return Map<String, List<String>>.from(result.map((key, value) =>
|
||||||
|
MapEntry(key, (value as List<dynamic>).cast<String>().toList())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> formatYubiOtpCsv(
|
||||||
|
int serial, String publicId, String privateId, String key) async {
|
||||||
|
final result = await _session.command('format_yubiotp_csv',
|
||||||
|
target: _subpath,
|
||||||
|
params: {
|
||||||
|
'serial': serial,
|
||||||
|
'public_id': publicId,
|
||||||
|
'private_id': privateId,
|
||||||
|
'key': key
|
||||||
|
});
|
||||||
|
return result['csv'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteSlot(SlotId slot) async {
|
||||||
|
await _session.command('delete', target: [..._subpath, slot.id]);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> configureSlot(SlotId slot,
|
||||||
|
{required SlotConfiguration configuration}) async {
|
||||||
|
await _session.command('put',
|
||||||
|
target: [..._subpath, slot.id], params: configuration.toJson());
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../fido/models.dart';
|
import '../../fido/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../../widgets/utf8_utils.dart';
|
import '../../widgets/utf8_utils.dart';
|
||||||
@ -206,7 +207,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
inputFormatters: [limitBytesLength(15)],
|
inputFormatters: [limitBytesLength(15)],
|
||||||
buildCounter: buildByteCounterFor(_label),
|
buildCounter: buildByteCounterFor(_label),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
enabled: _fingerprint != null,
|
enabled: _fingerprint != null,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_name,
|
labelText: l10n.s_name,
|
||||||
|
@ -23,6 +23,7 @@ import '../../app/views/app_page.dart';
|
|||||||
import '../../app/views/graphics.dart';
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../features.dart' as features;
|
import '../features.dart' as features;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -166,7 +167,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
|||||||
obscureText: _isObscure,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
controller: _pinController,
|
controller: _pinController,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
helperText: '', // Prevents dialog resizing
|
helperText: '', // Prevents dialog resizing
|
||||||
@ -175,9 +176,8 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
|||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||||
color: IconTheme.of(context).color,
|
color: !_pinIsWrong ? IconTheme.of(context).color : null),
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
|
@ -24,6 +24,7 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -48,6 +49,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
String? _newPinError;
|
String? _newPinError;
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
bool _newIsWrong = false;
|
bool _newIsWrong = false;
|
||||||
|
bool _isObscureCurrent = true;
|
||||||
|
bool _isObscureNew = true;
|
||||||
|
bool _isObscureConfirm = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -76,14 +80,26 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
AppTextFormField(
|
AppTextFormField(
|
||||||
initialValue: _currentPin,
|
initialValue: _currentPin,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscureCurrent,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_pin,
|
labelText: l10n.s_current_pin,
|
||||||
errorText: _currentIsWrong ? _currentPinError : null,
|
errorText: _currentIsWrong ? _currentPinError : null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureCurrent
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureCurrent = !_isObscureCurrent;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip:
|
||||||
|
_isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -98,15 +114,25 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
AppTextFormField(
|
AppTextFormField(
|
||||||
initialValue: _newPin,
|
initialValue: _newPin,
|
||||||
autofocus: !hasPin,
|
autofocus: !hasPin,
|
||||||
obscureText: true,
|
obscureText: _isObscureNew,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_new_pin,
|
labelText: l10n.s_new_pin,
|
||||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||||
errorText: _newIsWrong ? _newPinError : null,
|
errorText: _newIsWrong ? _newPinError : null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscureNew ? Icons.visibility : Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureNew = !_isObscureNew;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -117,12 +143,24 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
),
|
),
|
||||||
AppTextFormField(
|
AppTextFormField(
|
||||||
initialValue: _confirmPin,
|
initialValue: _confirmPin,
|
||||||
obscureText: true,
|
obscureText: _isObscureConfirm,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_confirm_pin,
|
labelText: l10n.s_confirm_pin,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureConfirm
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureConfirm = !_isObscureConfirm;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip:
|
||||||
|
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
|
),
|
||||||
enabled:
|
enabled:
|
||||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
||||||
),
|
),
|
||||||
|
@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../../widgets/utf8_utils.dart';
|
import '../../widgets/utf8_utils.dart';
|
||||||
@ -95,7 +96,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
|||||||
maxLength: 15,
|
maxLength: 15,
|
||||||
inputFormatters: [limitBytesLength(15)],
|
inputFormatters: [limitBytesLength(15)],
|
||||||
buildCounter: buildByteCounterFor(_label),
|
buildCounter: buildByteCounterFor(_label),
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_label,
|
labelText: l10n.s_label,
|
||||||
prefixIcon: const Icon(Icons.fingerprint_outlined),
|
prefixIcon: const Icon(Icons.fingerprint_outlined),
|
||||||
|
@ -43,64 +43,76 @@ class ResetDialog extends ConsumerStatefulWidget {
|
|||||||
class _ResetDialogState extends ConsumerState<ResetDialog> {
|
class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||||
StreamSubscription<InteractionEvent>? _subscription;
|
StreamSubscription<InteractionEvent>? _subscription;
|
||||||
InteractionEvent? _interaction;
|
InteractionEvent? _interaction;
|
||||||
|
int _currentStep = -1;
|
||||||
|
final _totalSteps = 3;
|
||||||
|
|
||||||
String _getMessage() {
|
String _getMessage() {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final nfc = widget.node.transport == Transport.nfc;
|
final nfc = widget.node.transport == Transport.nfc;
|
||||||
|
if (_currentStep == 3) {
|
||||||
|
return l10n.l_fido_app_reset;
|
||||||
|
}
|
||||||
return switch (_interaction) {
|
return switch (_interaction) {
|
||||||
InteractionEvent.remove =>
|
InteractionEvent.remove =>
|
||||||
nfc ? l10n.l_remove_yk_from_reader : l10n.l_unplug_yk,
|
nfc ? l10n.l_remove_yk_from_reader : l10n.l_unplug_yk,
|
||||||
InteractionEvent.insert =>
|
InteractionEvent.insert =>
|
||||||
nfc ? l10n.l_replace_yk_on_reader : l10n.l_reinsert_yk,
|
nfc ? l10n.l_replace_yk_on_reader : l10n.l_reinsert_yk,
|
||||||
InteractionEvent.touch => l10n.l_touch_button_now,
|
InteractionEvent.touch => l10n.l_touch_button_now,
|
||||||
null => l10n.l_press_reset_to_begin
|
null => ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
double progress = _currentStep == -1 ? 0.0 : _currentStep / (_totalSteps);
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(l10n.s_factory_reset),
|
title: Text(l10n.s_factory_reset),
|
||||||
onCancel: () {
|
onCancel: _currentStep < 3
|
||||||
_subscription?.cancel();
|
? () {
|
||||||
},
|
_subscription?.cancel();
|
||||||
actions: [
|
}
|
||||||
TextButton(
|
: null,
|
||||||
onPressed: _subscription == null
|
actions: _currentStep < 3
|
||||||
? () async {
|
? [
|
||||||
_subscription = ref
|
TextButton(
|
||||||
.read(fidoStateProvider(widget.node.path).notifier)
|
onPressed: _subscription == null
|
||||||
.reset()
|
? () async {
|
||||||
.listen((event) {
|
_subscription = ref
|
||||||
setState(() {
|
.read(fidoStateProvider(widget.node.path).notifier)
|
||||||
_interaction = event;
|
.reset()
|
||||||
});
|
.listen((event) {
|
||||||
}, onDone: () {
|
setState(() {
|
||||||
_subscription = null;
|
_currentStep++;
|
||||||
Navigator.of(context).pop();
|
_interaction = event;
|
||||||
showMessage(context, l10n.l_fido_app_reset);
|
});
|
||||||
}, onError: (e) {
|
}, onDone: () {
|
||||||
_log.error('Error performing FIDO reset', e);
|
setState(() {
|
||||||
Navigator.of(context).pop();
|
_currentStep++;
|
||||||
final String errorMessage;
|
});
|
||||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
_subscription = null;
|
||||||
if (e is RpcError) {
|
}, onError: (e) {
|
||||||
errorMessage = e.message;
|
_log.error('Error performing FIDO reset', e);
|
||||||
} else {
|
Navigator.of(context).pop();
|
||||||
errorMessage = e.toString();
|
final String errorMessage;
|
||||||
}
|
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||||
showMessage(
|
if (e is RpcError) {
|
||||||
context,
|
errorMessage = e.message;
|
||||||
l10n.l_reset_failed(errorMessage),
|
} else {
|
||||||
duration: const Duration(seconds: 4),
|
errorMessage = e.toString();
|
||||||
);
|
}
|
||||||
});
|
showMessage(
|
||||||
}
|
context,
|
||||||
: null,
|
l10n.l_reset_failed(errorMessage),
|
||||||
child: Text(l10n.s_reset),
|
duration: const Duration(seconds: 4),
|
||||||
),
|
);
|
||||||
],
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_reset),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -113,10 +125,10 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
|||||||
Text(
|
Text(
|
||||||
l10n.p_warning_disable_accounts,
|
l10n.p_warning_disable_accounts,
|
||||||
),
|
),
|
||||||
Center(
|
if (_currentStep > -1) ...[
|
||||||
child: Text(_getMessage(),
|
Text('${l10n.s_status}: ${_getMessage()}'),
|
||||||
style: Theme.of(context).textTheme.titleLarge),
|
LinearProgressIndicator(value: progress)
|
||||||
),
|
],
|
||||||
]
|
]
|
||||||
.map((e) => Padding(
|
.map((e) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"s_close": "Schließen",
|
"s_close": "Schließen",
|
||||||
"s_delete": "Löschen",
|
"s_delete": "Löschen",
|
||||||
"s_quit": "Beenden",
|
"s_quit": "Beenden",
|
||||||
|
"s_status": null,
|
||||||
"s_unlock": "Entsperren",
|
"s_unlock": "Entsperren",
|
||||||
"s_calculate": "Berechnen",
|
"s_calculate": "Berechnen",
|
||||||
"s_import": null,
|
"s_import": null,
|
||||||
@ -61,8 +62,9 @@
|
|||||||
"s_manage": "Verwalten",
|
"s_manage": "Verwalten",
|
||||||
"s_setup": "Einrichten",
|
"s_setup": "Einrichten",
|
||||||
"s_settings": "Einstellungen",
|
"s_settings": "Einstellungen",
|
||||||
"s_piv": null,
|
"s_certificates": null,
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
|
"s_slots": null,
|
||||||
"s_help_and_about": "Hilfe und Über",
|
"s_help_and_about": "Hilfe und Über",
|
||||||
"s_help_and_feedback": "Hilfe und Feedback",
|
"s_help_and_feedback": "Hilfe und Feedback",
|
||||||
"s_send_feedback": "Senden Sie uns Feedback",
|
"s_send_feedback": "Senden Sie uns Feedback",
|
||||||
@ -78,6 +80,14 @@
|
|||||||
"s_hide_secret_key": null,
|
"s_hide_secret_key": null,
|
||||||
"s_private_key": null,
|
"s_private_key": null,
|
||||||
"s_invalid_length": "Ungültige Länge",
|
"s_invalid_length": "Ungültige Länge",
|
||||||
|
"s_invalid_format": null,
|
||||||
|
"l_invalid_format_allowed_chars": null,
|
||||||
|
"@l_invalid_format_allowed_chars": {
|
||||||
|
"placeholders": {
|
||||||
|
"characters": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_invalid_keyboard_character": null,
|
||||||
"s_require_touch": "Berührung ist erforderlich",
|
"s_require_touch": "Berührung ist erforderlich",
|
||||||
"q_have_account_info": "Haben Sie Konto-Informationen?",
|
"q_have_account_info": "Haben Sie Konto-Informationen?",
|
||||||
"s_run_diagnostics": "Diagnose ausführen",
|
"s_run_diagnostics": "Diagnose ausführen",
|
||||||
@ -189,6 +199,8 @@
|
|||||||
"s_change_puk": null,
|
"s_change_puk": null,
|
||||||
"s_show_pin": null,
|
"s_show_pin": null,
|
||||||
"s_hide_pin": null,
|
"s_hide_pin": null,
|
||||||
|
"s_show_puk": null,
|
||||||
|
"s_hide_puk": null,
|
||||||
"s_current_pin": "Derzeitige PIN",
|
"s_current_pin": "Derzeitige PIN",
|
||||||
"s_current_puk": null,
|
"s_current_puk": null,
|
||||||
"s_new_pin": "Neue PIN",
|
"s_new_pin": "Neue PIN",
|
||||||
@ -286,6 +298,8 @@
|
|||||||
"s_management_key": null,
|
"s_management_key": null,
|
||||||
"s_current_management_key": null,
|
"s_current_management_key": null,
|
||||||
"s_new_management_key": null,
|
"s_new_management_key": null,
|
||||||
|
"s_show_management_key": null,
|
||||||
|
"s_hide_management_key": null,
|
||||||
"l_change_management_key": null,
|
"l_change_management_key": null,
|
||||||
"p_change_management_key_desc": null,
|
"p_change_management_key_desc": null,
|
||||||
"l_management_key_changed": null,
|
"l_management_key_changed": null,
|
||||||
@ -429,7 +443,6 @@
|
|||||||
|
|
||||||
"@_certificates": {},
|
"@_certificates": {},
|
||||||
"s_certificate": null,
|
"s_certificate": null,
|
||||||
"s_certificates": null,
|
|
||||||
"s_csr": null,
|
"s_csr": null,
|
||||||
"s_subject": null,
|
"s_subject": null,
|
||||||
"l_export_csr_file": null,
|
"l_export_csr_file": null,
|
||||||
@ -504,6 +517,74 @@
|
|||||||
"s_slot_9d": null,
|
"s_slot_9d": null,
|
||||||
"s_slot_9e": null,
|
"s_slot_9e": null,
|
||||||
|
|
||||||
|
"@_otp_slots": {},
|
||||||
|
"s_otp_slot_one": null,
|
||||||
|
"s_otp_slot_two": null,
|
||||||
|
"l_otp_slot_empty": null,
|
||||||
|
"l_otp_slot_configured": null,
|
||||||
|
|
||||||
|
"@_otp_slot_configurations": {},
|
||||||
|
"s_yubiotp": null,
|
||||||
|
"l_yubiotp_desc": null,
|
||||||
|
"s_challenge_response": null,
|
||||||
|
"l_challenge_response_desc": null,
|
||||||
|
"s_static_password": null,
|
||||||
|
"l_static_password_desc": null,
|
||||||
|
"s_hotp": null,
|
||||||
|
"l_hotp_desc": null,
|
||||||
|
"s_public_id": null,
|
||||||
|
"s_private_id": null,
|
||||||
|
"s_allow_any_character": null,
|
||||||
|
"s_use_serial": null,
|
||||||
|
"s_generate_private_id": null,
|
||||||
|
"s_generate_secret_key": null,
|
||||||
|
"s_generate_passowrd": null,
|
||||||
|
"l_select_file": null,
|
||||||
|
"l_no_export_file": null,
|
||||||
|
"s_no_export": null,
|
||||||
|
"s_export": null,
|
||||||
|
"l_export_configuration_file": null,
|
||||||
|
|
||||||
|
"@_otp_slot_actions": {},
|
||||||
|
"s_delete_slot": null,
|
||||||
|
"l_delete_slot_desc": null,
|
||||||
|
"p_warning_delete_slot_configuration": null,
|
||||||
|
"@p_warning_delete_slot_configuration": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot_id": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_deleted": null,
|
||||||
|
"s_swap": null,
|
||||||
|
"s_swap_slots": null,
|
||||||
|
"l_swap_slots_desc": null,
|
||||||
|
"p_swap_slots_desc": null,
|
||||||
|
"l_slots_swapped": null,
|
||||||
|
"l_slot_credential_configured": null,
|
||||||
|
"@l_slot_credential_configured": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_credential_configured_and_exported": null,
|
||||||
|
"@l_slot_credential_configured_and_exported": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {},
|
||||||
|
"file": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s_append_enter": null,
|
||||||
|
"l_append_enter_desc": null,
|
||||||
|
|
||||||
|
"@_otp_errors": {},
|
||||||
|
"p_otp_slot_configuration_error": null,
|
||||||
|
"@p_otp_slot_configuration_error": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
"@_permissions": {},
|
"@_permissions": {},
|
||||||
"s_enable_nfc": "NFC aktivieren",
|
"s_enable_nfc": "NFC aktivieren",
|
||||||
"s_permission_denied": "Zugriff verweigert",
|
"s_permission_denied": "Zugriff verweigert",
|
||||||
@ -539,7 +620,6 @@
|
|||||||
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
|
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
|
||||||
"s_reset_fido": "FIDO zurücksetzen",
|
"s_reset_fido": "FIDO zurücksetzen",
|
||||||
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
|
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
|
||||||
"l_press_reset_to_begin": "Drücken Sie Zurücksetzen um zu beginnen\u2026",
|
|
||||||
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
|
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
|
||||||
"@l_reset_failed": {
|
"@l_reset_failed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"s_close": "Close",
|
"s_close": "Close",
|
||||||
"s_delete": "Delete",
|
"s_delete": "Delete",
|
||||||
"s_quit": "Quit",
|
"s_quit": "Quit",
|
||||||
|
"s_status": "Status",
|
||||||
"s_unlock": "Unlock",
|
"s_unlock": "Unlock",
|
||||||
"s_calculate": "Calculate",
|
"s_calculate": "Calculate",
|
||||||
"s_import": "Import",
|
"s_import": "Import",
|
||||||
@ -61,8 +62,9 @@
|
|||||||
"s_manage": "Manage",
|
"s_manage": "Manage",
|
||||||
"s_setup": "Setup",
|
"s_setup": "Setup",
|
||||||
"s_settings": "Settings",
|
"s_settings": "Settings",
|
||||||
"s_piv": "PIV",
|
"s_certificates": "Certificates",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
|
"s_slots": "Slots",
|
||||||
"s_help_and_about": "Help and about",
|
"s_help_and_about": "Help and about",
|
||||||
"s_help_and_feedback": "Help and feedback",
|
"s_help_and_feedback": "Help and feedback",
|
||||||
"s_send_feedback": "Send us feedback",
|
"s_send_feedback": "Send us feedback",
|
||||||
@ -78,6 +80,14 @@
|
|||||||
"s_hide_secret_key": "Hide secret key",
|
"s_hide_secret_key": "Hide secret key",
|
||||||
"s_private_key": "Private key",
|
"s_private_key": "Private key",
|
||||||
"s_invalid_length": "Invalid length",
|
"s_invalid_length": "Invalid length",
|
||||||
|
"s_invalid_format": "Invalid format",
|
||||||
|
"l_invalid_format_allowed_chars": "Invalid format, allowed characters: {characters}",
|
||||||
|
"@l_invalid_format_allowed_chars": {
|
||||||
|
"placeholders": {
|
||||||
|
"characters": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_invalid_keyboard_character": "Invalid characters for selected keyboard",
|
||||||
"s_require_touch": "Require touch",
|
"s_require_touch": "Require touch",
|
||||||
"q_have_account_info": "Have account info?",
|
"q_have_account_info": "Have account info?",
|
||||||
"s_run_diagnostics": "Run diagnostics",
|
"s_run_diagnostics": "Run diagnostics",
|
||||||
@ -189,6 +199,8 @@
|
|||||||
"s_change_puk": "Change PUK",
|
"s_change_puk": "Change PUK",
|
||||||
"s_show_pin": "Show PIN",
|
"s_show_pin": "Show PIN",
|
||||||
"s_hide_pin": "Hide PIN",
|
"s_hide_pin": "Hide PIN",
|
||||||
|
"s_show_puk": "Show PUK",
|
||||||
|
"s_hide_puk": "Hide PUK",
|
||||||
"s_current_pin": "Current PIN",
|
"s_current_pin": "Current PIN",
|
||||||
"s_current_puk": "Current PUK",
|
"s_current_puk": "Current PUK",
|
||||||
"s_new_pin": "New PIN",
|
"s_new_pin": "New PIN",
|
||||||
@ -286,6 +298,8 @@
|
|||||||
"s_management_key": "Management key",
|
"s_management_key": "Management key",
|
||||||
"s_current_management_key": "Current management key",
|
"s_current_management_key": "Current management key",
|
||||||
"s_new_management_key": "New management key",
|
"s_new_management_key": "New management key",
|
||||||
|
"s_show_management_key": "Show management key",
|
||||||
|
"s_hide_management_key": "Hide management key",
|
||||||
"l_change_management_key": "Change management key",
|
"l_change_management_key": "Change management key",
|
||||||
"p_change_management_key_desc": "Change your management key. You can optionally choose to allow the PIN to be used instead of the management key.",
|
"p_change_management_key_desc": "Change your management key. You can optionally choose to allow the PIN to be used instead of the management key.",
|
||||||
"l_management_key_changed": "Management key changed",
|
"l_management_key_changed": "Management key changed",
|
||||||
@ -429,7 +443,6 @@
|
|||||||
|
|
||||||
"@_certificates": {},
|
"@_certificates": {},
|
||||||
"s_certificate": "Certificate",
|
"s_certificate": "Certificate",
|
||||||
"s_certificates": "Certificates",
|
|
||||||
"s_csr": "CSR",
|
"s_csr": "CSR",
|
||||||
"s_subject": "Subject",
|
"s_subject": "Subject",
|
||||||
"l_export_csr_file": "Save CSR to file",
|
"l_export_csr_file": "Save CSR to file",
|
||||||
@ -504,6 +517,74 @@
|
|||||||
"s_slot_9d": "Key Management",
|
"s_slot_9d": "Key Management",
|
||||||
"s_slot_9e": "Card Authentication",
|
"s_slot_9e": "Card Authentication",
|
||||||
|
|
||||||
|
"@_otp_slots": {},
|
||||||
|
"s_otp_slot_one": "Short touch",
|
||||||
|
"s_otp_slot_two": "Long touch",
|
||||||
|
"l_otp_slot_empty": "Slot is empty",
|
||||||
|
"l_otp_slot_configured": "Slot is configured",
|
||||||
|
|
||||||
|
"@_otp_slot_configurations": {},
|
||||||
|
"s_yubiotp": "Yubico OTP",
|
||||||
|
"l_yubiotp_desc": "Program a Yubico OTP credential",
|
||||||
|
"s_challenge_response": "Challenge-response",
|
||||||
|
"l_challenge_response_desc": "Program a challenge-response credential",
|
||||||
|
"s_static_password": "Static password",
|
||||||
|
"l_static_password_desc": "Configure a static password",
|
||||||
|
"s_hotp": "OATH-HOTP",
|
||||||
|
"l_hotp_desc": "Program a HMAC-SHA1 based credential",
|
||||||
|
"s_public_id": "Public ID",
|
||||||
|
"s_private_id": "Private ID",
|
||||||
|
"s_allow_any_character": "Allow any character",
|
||||||
|
"s_use_serial": "Use serial",
|
||||||
|
"s_generate_private_id": "Generate private ID",
|
||||||
|
"s_generate_secret_key": "Generate secret key",
|
||||||
|
"s_generate_passowrd": "Generate password",
|
||||||
|
"l_select_file": "Select file",
|
||||||
|
"l_no_export_file": "No export file",
|
||||||
|
"s_no_export": "No export",
|
||||||
|
"s_export": "Export",
|
||||||
|
"l_export_configuration_file": "Export configuration to file",
|
||||||
|
|
||||||
|
"@_otp_slot_actions": {},
|
||||||
|
"s_delete_slot": "Delete credential",
|
||||||
|
"l_delete_slot_desc": "Remove credential in slot",
|
||||||
|
"p_warning_delete_slot_configuration": "Warning! This action will permanently remove the credential from slot {slot_id}.",
|
||||||
|
"@p_warning_delete_slot_configuration": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot_id": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_deleted": "Credential deleted",
|
||||||
|
"s_swap": "Swap",
|
||||||
|
"s_swap_slots": "Swap slots",
|
||||||
|
"l_swap_slots_desc": "Swap short/long touch",
|
||||||
|
"p_swap_slots_desc": "This will swap the configuration of the two slots.",
|
||||||
|
"l_slots_swapped": "Slot configurations swapped",
|
||||||
|
"l_slot_credential_configured": "Configured {type} credential",
|
||||||
|
"@l_slot_credential_configured": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_credential_configured_and_exported": "Configured {type} credential and exported to {file}",
|
||||||
|
"@l_slot_credential_configured_and_exported": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {},
|
||||||
|
"file": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s_append_enter": "Append ⏎",
|
||||||
|
"l_append_enter_desc": "Append an Enter keystroke after emitting the OTP",
|
||||||
|
|
||||||
|
"@_otp_errors": {},
|
||||||
|
"p_otp_slot_configuration_error": "Failed to modify {slot}! Make sure the YubiKey does not have restrictive access.",
|
||||||
|
"@p_otp_slot_configuration_error": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
"@_permissions": {},
|
"@_permissions": {},
|
||||||
"s_enable_nfc": "Enable NFC",
|
"s_enable_nfc": "Enable NFC",
|
||||||
"s_permission_denied": "Permission denied",
|
"s_permission_denied": "Permission denied",
|
||||||
@ -539,7 +620,6 @@
|
|||||||
"l_oath_application_reset": "OATH application reset",
|
"l_oath_application_reset": "OATH application reset",
|
||||||
"s_reset_fido": "Reset FIDO",
|
"s_reset_fido": "Reset FIDO",
|
||||||
"l_fido_app_reset": "FIDO application reset",
|
"l_fido_app_reset": "FIDO application reset",
|
||||||
"l_press_reset_to_begin": "Press reset to begin\u2026",
|
|
||||||
"l_reset_failed": "Error performing reset: {message}",
|
"l_reset_failed": "Error performing reset: {message}",
|
||||||
"@l_reset_failed": {
|
"@l_reset_failed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"s_close": "Fermer",
|
"s_close": "Fermer",
|
||||||
"s_delete": "Supprimer",
|
"s_delete": "Supprimer",
|
||||||
"s_quit": "Quitter",
|
"s_quit": "Quitter",
|
||||||
|
"s_status": null,
|
||||||
"s_unlock": "Déverrouiller",
|
"s_unlock": "Déverrouiller",
|
||||||
"s_calculate": "Calculer",
|
"s_calculate": "Calculer",
|
||||||
"s_import": "Importer",
|
"s_import": "Importer",
|
||||||
@ -61,8 +62,9 @@
|
|||||||
"s_manage": "Gérer",
|
"s_manage": "Gérer",
|
||||||
"s_setup": "Configuration",
|
"s_setup": "Configuration",
|
||||||
"s_settings": "Paramètres",
|
"s_settings": "Paramètres",
|
||||||
"s_piv": "PIV",
|
"s_certificates": "Certificats",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
|
"s_slots": null,
|
||||||
"s_help_and_about": "Aide et à propos",
|
"s_help_and_about": "Aide et à propos",
|
||||||
"s_help_and_feedback": "Aide et retours",
|
"s_help_and_feedback": "Aide et retours",
|
||||||
"s_send_feedback": "Envoyer nous un retour",
|
"s_send_feedback": "Envoyer nous un retour",
|
||||||
@ -78,6 +80,14 @@
|
|||||||
"s_hide_secret_key": null,
|
"s_hide_secret_key": null,
|
||||||
"s_private_key": "Clé privée",
|
"s_private_key": "Clé privée",
|
||||||
"s_invalid_length": "Longueur invalide",
|
"s_invalid_length": "Longueur invalide",
|
||||||
|
"s_invalid_format": null,
|
||||||
|
"l_invalid_format_allowed_chars": null,
|
||||||
|
"@l_invalid_format_allowed_chars": {
|
||||||
|
"placeholders": {
|
||||||
|
"characters": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_invalid_keyboard_character": null,
|
||||||
"s_require_touch": "Touché requis",
|
"s_require_touch": "Touché requis",
|
||||||
"q_have_account_info": "Avez-vous des informations de compte?",
|
"q_have_account_info": "Avez-vous des informations de compte?",
|
||||||
"s_run_diagnostics": "Exécuter un diagnostique",
|
"s_run_diagnostics": "Exécuter un diagnostique",
|
||||||
@ -189,6 +199,8 @@
|
|||||||
"s_change_puk": "Changez PUK",
|
"s_change_puk": "Changez PUK",
|
||||||
"s_show_pin": null,
|
"s_show_pin": null,
|
||||||
"s_hide_pin": null,
|
"s_hide_pin": null,
|
||||||
|
"s_show_puk": null,
|
||||||
|
"s_hide_puk": null,
|
||||||
"s_current_pin": "PIN actuel",
|
"s_current_pin": "PIN actuel",
|
||||||
"s_current_puk": "PUK actuel",
|
"s_current_puk": "PUK actuel",
|
||||||
"s_new_pin": "Nouveau PIN",
|
"s_new_pin": "Nouveau PIN",
|
||||||
@ -286,6 +298,8 @@
|
|||||||
"s_management_key": "Gestion des clés",
|
"s_management_key": "Gestion des clés",
|
||||||
"s_current_management_key": "Clé actuelle de gestion",
|
"s_current_management_key": "Clé actuelle de gestion",
|
||||||
"s_new_management_key": "Nouvelle clé de gestion",
|
"s_new_management_key": "Nouvelle clé de gestion",
|
||||||
|
"s_show_management_key": null,
|
||||||
|
"s_hide_management_key": null,
|
||||||
"l_change_management_key": "Changer la clé de gestion",
|
"l_change_management_key": "Changer la clé de gestion",
|
||||||
"p_change_management_key_desc": "Changer votre clé de gestion. Vous pouvez optionnellement autoriser le PIN à être utilisé à la place de la clé de gestion.",
|
"p_change_management_key_desc": "Changer votre clé de gestion. Vous pouvez optionnellement autoriser le PIN à être utilisé à la place de la clé de gestion.",
|
||||||
"l_management_key_changed": "Ché de gestion changée",
|
"l_management_key_changed": "Ché de gestion changée",
|
||||||
@ -429,7 +443,6 @@
|
|||||||
|
|
||||||
"@_certificates": {},
|
"@_certificates": {},
|
||||||
"s_certificate": "Certificat",
|
"s_certificate": "Certificat",
|
||||||
"s_certificates": "Certificats",
|
|
||||||
"s_csr": "CSR",
|
"s_csr": "CSR",
|
||||||
"s_subject": "Sujet",
|
"s_subject": "Sujet",
|
||||||
"l_export_csr_file": "Sauvegarder le CSR vers un fichier",
|
"l_export_csr_file": "Sauvegarder le CSR vers un fichier",
|
||||||
@ -504,6 +517,74 @@
|
|||||||
"s_slot_9d": "Gestion des clés",
|
"s_slot_9d": "Gestion des clés",
|
||||||
"s_slot_9e": "Authentification par carte",
|
"s_slot_9e": "Authentification par carte",
|
||||||
|
|
||||||
|
"@_otp_slots": {},
|
||||||
|
"s_otp_slot_one": null,
|
||||||
|
"s_otp_slot_two": null,
|
||||||
|
"l_otp_slot_empty": null,
|
||||||
|
"l_otp_slot_configured": null,
|
||||||
|
|
||||||
|
"@_otp_slot_configurations": {},
|
||||||
|
"s_yubiotp": null,
|
||||||
|
"l_yubiotp_desc": null,
|
||||||
|
"s_challenge_response": null,
|
||||||
|
"l_challenge_response_desc": null,
|
||||||
|
"s_static_password": null,
|
||||||
|
"l_static_password_desc": null,
|
||||||
|
"s_hotp": null,
|
||||||
|
"l_hotp_desc": null,
|
||||||
|
"s_public_id": null,
|
||||||
|
"s_private_id": null,
|
||||||
|
"s_allow_any_character": null,
|
||||||
|
"s_use_serial": null,
|
||||||
|
"s_generate_private_id": null,
|
||||||
|
"s_generate_secret_key": null,
|
||||||
|
"s_generate_passowrd": null,
|
||||||
|
"l_select_file": null,
|
||||||
|
"l_no_export_file": null,
|
||||||
|
"s_no_export": null,
|
||||||
|
"s_export": null,
|
||||||
|
"l_export_configuration_file": null,
|
||||||
|
|
||||||
|
"@_otp_slot_actions": {},
|
||||||
|
"s_delete_slot": null,
|
||||||
|
"l_delete_slot_desc": null,
|
||||||
|
"p_warning_delete_slot_configuration": null,
|
||||||
|
"@p_warning_delete_slot_configuration": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot_id": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_deleted": null,
|
||||||
|
"s_swap": null,
|
||||||
|
"s_swap_slots": null,
|
||||||
|
"l_swap_slots_desc": null,
|
||||||
|
"p_swap_slots_desc": null,
|
||||||
|
"l_slots_swapped": null,
|
||||||
|
"l_slot_credential_configured": null,
|
||||||
|
"@l_slot_credential_configured": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_credential_configured_and_exported": null,
|
||||||
|
"@l_slot_credential_configured_and_exported": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {},
|
||||||
|
"file": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s_append_enter": null,
|
||||||
|
"l_append_enter_desc": null,
|
||||||
|
|
||||||
|
"@_otp_errors": {},
|
||||||
|
"p_otp_slot_configuration_error": null,
|
||||||
|
"@p_otp_slot_configuration_error": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
"@_permissions": {},
|
"@_permissions": {},
|
||||||
"s_enable_nfc": "Activer le NFC",
|
"s_enable_nfc": "Activer le NFC",
|
||||||
"s_permission_denied": "Permission refusée",
|
"s_permission_denied": "Permission refusée",
|
||||||
@ -539,7 +620,6 @@
|
|||||||
"l_oath_application_reset": "L'application OATH à été réinitialisée",
|
"l_oath_application_reset": "L'application OATH à été réinitialisée",
|
||||||
"s_reset_fido": "Réinitialiser le FIDO",
|
"s_reset_fido": "Réinitialiser le FIDO",
|
||||||
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
|
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
|
||||||
"l_press_reset_to_begin": "Appuyez sur réinitialiser pour commencer\u2026",
|
|
||||||
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
|
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
|
||||||
"@l_reset_failed": {
|
"@l_reset_failed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"s_close": "閉じる",
|
"s_close": "閉じる",
|
||||||
"s_delete": "消去",
|
"s_delete": "消去",
|
||||||
"s_quit": "終了",
|
"s_quit": "終了",
|
||||||
|
"s_status": null,
|
||||||
"s_unlock": "ロック解除",
|
"s_unlock": "ロック解除",
|
||||||
"s_calculate": "計算",
|
"s_calculate": "計算",
|
||||||
"s_import": "インポート",
|
"s_import": "インポート",
|
||||||
@ -61,8 +62,9 @@
|
|||||||
"s_manage": "管理",
|
"s_manage": "管理",
|
||||||
"s_setup": "セットアップ",
|
"s_setup": "セットアップ",
|
||||||
"s_settings": "設定",
|
"s_settings": "設定",
|
||||||
"s_piv": "PIV",
|
"s_certificates": "証明書",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
|
"s_slots": null,
|
||||||
"s_help_and_about": "ヘルプと概要",
|
"s_help_and_about": "ヘルプと概要",
|
||||||
"s_help_and_feedback": "ヘルプとフィードバック",
|
"s_help_and_feedback": "ヘルプとフィードバック",
|
||||||
"s_send_feedback": "フィードバックの送信",
|
"s_send_feedback": "フィードバックの送信",
|
||||||
@ -78,6 +80,14 @@
|
|||||||
"s_hide_secret_key": null,
|
"s_hide_secret_key": null,
|
||||||
"s_private_key": "秘密鍵",
|
"s_private_key": "秘密鍵",
|
||||||
"s_invalid_length": "無効な長さです",
|
"s_invalid_length": "無効な長さです",
|
||||||
|
"s_invalid_format": null,
|
||||||
|
"l_invalid_format_allowed_chars": null,
|
||||||
|
"@l_invalid_format_allowed_chars": {
|
||||||
|
"placeholders": {
|
||||||
|
"characters": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_invalid_keyboard_character": null,
|
||||||
"s_require_touch": "タッチが必要",
|
"s_require_touch": "タッチが必要",
|
||||||
"q_have_account_info": "アカウント情報をお持ちですか?",
|
"q_have_account_info": "アカウント情報をお持ちですか?",
|
||||||
"s_run_diagnostics": "診断を実行する",
|
"s_run_diagnostics": "診断を実行する",
|
||||||
@ -189,6 +199,8 @@
|
|||||||
"s_change_puk": "PUKを変更する",
|
"s_change_puk": "PUKを変更する",
|
||||||
"s_show_pin": null,
|
"s_show_pin": null,
|
||||||
"s_hide_pin": null,
|
"s_hide_pin": null,
|
||||||
|
"s_show_puk": null,
|
||||||
|
"s_hide_puk": null,
|
||||||
"s_current_pin": "現在のPIN",
|
"s_current_pin": "現在のPIN",
|
||||||
"s_current_puk": "現在のPUK",
|
"s_current_puk": "現在のPUK",
|
||||||
"s_new_pin": "新しいPIN",
|
"s_new_pin": "新しいPIN",
|
||||||
@ -286,6 +298,8 @@
|
|||||||
"s_management_key": "Management key",
|
"s_management_key": "Management key",
|
||||||
"s_current_management_key": "現在のManagement key",
|
"s_current_management_key": "現在のManagement key",
|
||||||
"s_new_management_key": "新しいManagement key",
|
"s_new_management_key": "新しいManagement key",
|
||||||
|
"s_show_management_key": null,
|
||||||
|
"s_hide_management_key": null,
|
||||||
"l_change_management_key": "Management keyの変更",
|
"l_change_management_key": "Management keyの変更",
|
||||||
"p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です",
|
"p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です",
|
||||||
"l_management_key_changed": "Management keyは変更されました",
|
"l_management_key_changed": "Management keyは変更されました",
|
||||||
@ -429,7 +443,6 @@
|
|||||||
|
|
||||||
"@_certificates": {},
|
"@_certificates": {},
|
||||||
"s_certificate": "証明書",
|
"s_certificate": "証明書",
|
||||||
"s_certificates": "証明書",
|
|
||||||
"s_csr": "CSR",
|
"s_csr": "CSR",
|
||||||
"s_subject": "サブジェクト",
|
"s_subject": "サブジェクト",
|
||||||
"l_export_csr_file": "CSRをファイルに保存",
|
"l_export_csr_file": "CSRをファイルに保存",
|
||||||
@ -504,6 +517,74 @@
|
|||||||
"s_slot_9d": "鍵の管理",
|
"s_slot_9d": "鍵の管理",
|
||||||
"s_slot_9e": "カード認証",
|
"s_slot_9e": "カード認証",
|
||||||
|
|
||||||
|
"@_otp_slots": {},
|
||||||
|
"s_otp_slot_one": null,
|
||||||
|
"s_otp_slot_two": null,
|
||||||
|
"l_otp_slot_empty": null,
|
||||||
|
"l_otp_slot_configured": null,
|
||||||
|
|
||||||
|
"@_otp_slot_configurations": {},
|
||||||
|
"s_yubiotp": null,
|
||||||
|
"l_yubiotp_desc": null,
|
||||||
|
"s_challenge_response": null,
|
||||||
|
"l_challenge_response_desc": null,
|
||||||
|
"s_static_password": null,
|
||||||
|
"l_static_password_desc": null,
|
||||||
|
"s_hotp": null,
|
||||||
|
"l_hotp_desc": null,
|
||||||
|
"s_public_id": null,
|
||||||
|
"s_private_id": null,
|
||||||
|
"s_allow_any_character": null,
|
||||||
|
"s_use_serial": null,
|
||||||
|
"s_generate_private_id": null,
|
||||||
|
"s_generate_secret_key": null,
|
||||||
|
"s_generate_passowrd": null,
|
||||||
|
"l_select_file": null,
|
||||||
|
"l_no_export_file": null,
|
||||||
|
"s_no_export": null,
|
||||||
|
"s_export": null,
|
||||||
|
"l_export_configuration_file": null,
|
||||||
|
|
||||||
|
"@_otp_slot_actions": {},
|
||||||
|
"s_delete_slot": null,
|
||||||
|
"l_delete_slot_desc": null,
|
||||||
|
"p_warning_delete_slot_configuration": null,
|
||||||
|
"@p_warning_delete_slot_configuration": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot_id": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_deleted": null,
|
||||||
|
"s_swap": null,
|
||||||
|
"s_swap_slots": null,
|
||||||
|
"l_swap_slots_desc": null,
|
||||||
|
"p_swap_slots_desc": null,
|
||||||
|
"l_slots_swapped": null,
|
||||||
|
"l_slot_credential_configured": null,
|
||||||
|
"@l_slot_credential_configured": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_credential_configured_and_exported": null,
|
||||||
|
"@l_slot_credential_configured_and_exported": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {},
|
||||||
|
"file": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s_append_enter": null,
|
||||||
|
"l_append_enter_desc": null,
|
||||||
|
|
||||||
|
"@_otp_errors": {},
|
||||||
|
"p_otp_slot_configuration_error": null,
|
||||||
|
"@p_otp_slot_configuration_error": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
"@_permissions": {},
|
"@_permissions": {},
|
||||||
"s_enable_nfc": "NFCを有効にする",
|
"s_enable_nfc": "NFCを有効にする",
|
||||||
"s_permission_denied": "権限がありません",
|
"s_permission_denied": "権限がありません",
|
||||||
@ -539,7 +620,6 @@
|
|||||||
"l_oath_application_reset": "OATHアプリケーションのリセット",
|
"l_oath_application_reset": "OATHアプリケーションのリセット",
|
||||||
"s_reset_fido": "FIDOのリセット",
|
"s_reset_fido": "FIDOのリセット",
|
||||||
"l_fido_app_reset": "FIDOアプリケーションのリセット",
|
"l_fido_app_reset": "FIDOアプリケーションのリセット",
|
||||||
"l_press_reset_to_begin": "リセットを押して開始してください\u2026",
|
|
||||||
"l_reset_failed": "リセット実行中のエラー:{message}",
|
"l_reset_failed": "リセット実行中のエラー:{message}",
|
||||||
"@l_reset_failed": {
|
"@l_reset_failed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"s_close": "Zamknij",
|
"s_close": "Zamknij",
|
||||||
"s_delete": "Usuń",
|
"s_delete": "Usuń",
|
||||||
"s_quit": "Wyjdź",
|
"s_quit": "Wyjdź",
|
||||||
|
"s_status": null,
|
||||||
"s_unlock": "Odblokuj",
|
"s_unlock": "Odblokuj",
|
||||||
"s_calculate": "Oblicz",
|
"s_calculate": "Oblicz",
|
||||||
"s_import": "Importuj",
|
"s_import": "Importuj",
|
||||||
@ -61,8 +62,9 @@
|
|||||||
"s_manage": "Zarządzaj",
|
"s_manage": "Zarządzaj",
|
||||||
"s_setup": "Konfiguruj",
|
"s_setup": "Konfiguruj",
|
||||||
"s_settings": "Ustawienia",
|
"s_settings": "Ustawienia",
|
||||||
"s_piv": "PIV",
|
"s_certificates": "Certyfikaty",
|
||||||
"s_webauthn": "WebAuthn",
|
"s_webauthn": "WebAuthn",
|
||||||
|
"s_slots": null,
|
||||||
"s_help_and_about": "Pomoc i informacje",
|
"s_help_and_about": "Pomoc i informacje",
|
||||||
"s_help_and_feedback": "Pomoc i opinie",
|
"s_help_and_feedback": "Pomoc i opinie",
|
||||||
"s_send_feedback": "Prześlij opinię",
|
"s_send_feedback": "Prześlij opinię",
|
||||||
@ -78,6 +80,14 @@
|
|||||||
"s_hide_secret_key": "Ukryj tajny klucz",
|
"s_hide_secret_key": "Ukryj tajny klucz",
|
||||||
"s_private_key": "Klucz prywatny",
|
"s_private_key": "Klucz prywatny",
|
||||||
"s_invalid_length": "Nieprawidłowa długość",
|
"s_invalid_length": "Nieprawidłowa długość",
|
||||||
|
"s_invalid_format": null,
|
||||||
|
"l_invalid_format_allowed_chars": null,
|
||||||
|
"@l_invalid_format_allowed_chars": {
|
||||||
|
"placeholders": {
|
||||||
|
"characters": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_invalid_keyboard_character": null,
|
||||||
"s_require_touch": "Wymagaj dotknięcia",
|
"s_require_touch": "Wymagaj dotknięcia",
|
||||||
"q_have_account_info": "Masz dane konta?",
|
"q_have_account_info": "Masz dane konta?",
|
||||||
"s_run_diagnostics": "Uruchom diagnostykę",
|
"s_run_diagnostics": "Uruchom diagnostykę",
|
||||||
@ -189,6 +199,8 @@
|
|||||||
"s_change_puk": "Zmień PUK",
|
"s_change_puk": "Zmień PUK",
|
||||||
"s_show_pin": "Pokaż PIN",
|
"s_show_pin": "Pokaż PIN",
|
||||||
"s_hide_pin": "Ukryj PIN",
|
"s_hide_pin": "Ukryj PIN",
|
||||||
|
"s_show_puk": null,
|
||||||
|
"s_hide_puk": null,
|
||||||
"s_current_pin": "Aktualny PIN",
|
"s_current_pin": "Aktualny PIN",
|
||||||
"s_current_puk": "Aktualny PUK",
|
"s_current_puk": "Aktualny PUK",
|
||||||
"s_new_pin": "Nowy PIN",
|
"s_new_pin": "Nowy PIN",
|
||||||
@ -286,6 +298,8 @@
|
|||||||
"s_management_key": "Klucz zarządzania",
|
"s_management_key": "Klucz zarządzania",
|
||||||
"s_current_management_key": "Aktualny klucz zarządzania",
|
"s_current_management_key": "Aktualny klucz zarządzania",
|
||||||
"s_new_management_key": "Nowy klucz zarządzania",
|
"s_new_management_key": "Nowy klucz zarządzania",
|
||||||
|
"s_show_management_key": null,
|
||||||
|
"s_hide_management_key": null,
|
||||||
"l_change_management_key": "Zmień klucz zarządzania",
|
"l_change_management_key": "Zmień klucz zarządzania",
|
||||||
"p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.",
|
"p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.",
|
||||||
"l_management_key_changed": "Zmieniono klucz zarządzania",
|
"l_management_key_changed": "Zmieniono klucz zarządzania",
|
||||||
@ -429,7 +443,6 @@
|
|||||||
|
|
||||||
"@_certificates": {},
|
"@_certificates": {},
|
||||||
"s_certificate": "Certyfikat",
|
"s_certificate": "Certyfikat",
|
||||||
"s_certificates": "Certyfikaty",
|
|
||||||
"s_csr": "CSR",
|
"s_csr": "CSR",
|
||||||
"s_subject": "Temat",
|
"s_subject": "Temat",
|
||||||
"l_export_csr_file": "Zapisz CSR do pliku",
|
"l_export_csr_file": "Zapisz CSR do pliku",
|
||||||
@ -504,6 +517,74 @@
|
|||||||
"s_slot_9d": "Menedżer kluczy",
|
"s_slot_9d": "Menedżer kluczy",
|
||||||
"s_slot_9e": "Autoryzacja karty",
|
"s_slot_9e": "Autoryzacja karty",
|
||||||
|
|
||||||
|
"@_otp_slots": {},
|
||||||
|
"s_otp_slot_one": null,
|
||||||
|
"s_otp_slot_two": null,
|
||||||
|
"l_otp_slot_empty": null,
|
||||||
|
"l_otp_slot_configured": null,
|
||||||
|
|
||||||
|
"@_otp_slot_configurations": {},
|
||||||
|
"s_yubiotp": null,
|
||||||
|
"l_yubiotp_desc": null,
|
||||||
|
"s_challenge_response": null,
|
||||||
|
"l_challenge_response_desc": null,
|
||||||
|
"s_static_password": null,
|
||||||
|
"l_static_password_desc": null,
|
||||||
|
"s_hotp": null,
|
||||||
|
"l_hotp_desc": null,
|
||||||
|
"s_public_id": null,
|
||||||
|
"s_private_id": null,
|
||||||
|
"s_allow_any_character": null,
|
||||||
|
"s_use_serial": null,
|
||||||
|
"s_generate_private_id": null,
|
||||||
|
"s_generate_secret_key": null,
|
||||||
|
"s_generate_passowrd": null,
|
||||||
|
"l_select_file": null,
|
||||||
|
"l_no_export_file": null,
|
||||||
|
"s_no_export": null,
|
||||||
|
"s_export": null,
|
||||||
|
"l_export_configuration_file": null,
|
||||||
|
|
||||||
|
"@_otp_slot_actions": {},
|
||||||
|
"s_delete_slot": null,
|
||||||
|
"l_delete_slot_desc": null,
|
||||||
|
"p_warning_delete_slot_configuration": null,
|
||||||
|
"@p_warning_delete_slot_configuration": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot_id": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_deleted": null,
|
||||||
|
"s_swap": null,
|
||||||
|
"s_swap_slots": null,
|
||||||
|
"l_swap_slots_desc": null,
|
||||||
|
"p_swap_slots_desc": null,
|
||||||
|
"l_slots_swapped": null,
|
||||||
|
"l_slot_credential_configured": null,
|
||||||
|
"@l_slot_credential_configured": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_slot_credential_configured_and_exported": null,
|
||||||
|
"@l_slot_credential_configured_and_exported": {
|
||||||
|
"placeholders": {
|
||||||
|
"type": {},
|
||||||
|
"file": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s_append_enter": null,
|
||||||
|
"l_append_enter_desc": null,
|
||||||
|
|
||||||
|
"@_otp_errors": {},
|
||||||
|
"p_otp_slot_configuration_error": null,
|
||||||
|
"@p_otp_slot_configuration_error": {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
"@_permissions": {},
|
"@_permissions": {},
|
||||||
"s_enable_nfc": "Włącz NFC",
|
"s_enable_nfc": "Włącz NFC",
|
||||||
"s_permission_denied": "Odmowa dostępu",
|
"s_permission_denied": "Odmowa dostępu",
|
||||||
@ -539,7 +620,6 @@
|
|||||||
"l_oath_application_reset": "Reset funkcji OATH",
|
"l_oath_application_reset": "Reset funkcji OATH",
|
||||||
"s_reset_fido": "Zresetuj FIDO",
|
"s_reset_fido": "Zresetuj FIDO",
|
||||||
"l_fido_app_reset": "Reset funkcji FIDO",
|
"l_fido_app_reset": "Reset funkcji FIDO",
|
||||||
"l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć\u2026",
|
|
||||||
"l_reset_failed": "Błąd podczas resetowania: {message}",
|
"l_reset_failed": "Błąd podczas resetowania: {message}",
|
||||||
"@l_reset_failed": {
|
"@l_reset_failed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -19,7 +19,6 @@ import 'dart:convert';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -30,11 +29,13 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/user_interaction.dart';
|
import '../../app/views/user_interaction.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../exception/apdu_exception.dart';
|
import '../../exception/apdu_exception.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/choice_filter_chip.dart';
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
import '../../widgets/file_drop_target.dart';
|
import '../../widgets/file_drop_target.dart';
|
||||||
@ -49,9 +50,6 @@ import 'utils.dart';
|
|||||||
|
|
||||||
final _log = Logger('oath.view.add_account_page');
|
final _log = Logger('oath.view.add_account_page');
|
||||||
|
|
||||||
final _secretFormatterPattern =
|
|
||||||
RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false);
|
|
||||||
|
|
||||||
class OathAddAccountPage extends ConsumerStatefulWidget {
|
class OathAddAccountPage extends ConsumerStatefulWidget {
|
||||||
final DevicePath? devicePath;
|
final DevicePath? devicePath;
|
||||||
final OathState? state;
|
final OathState? state;
|
||||||
@ -83,7 +81,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
||||||
int _digits = defaultDigits;
|
int _digits = defaultDigits;
|
||||||
int _counter = defaultCounter;
|
int _counter = defaultCounter;
|
||||||
bool _validateSecretLength = false;
|
bool _validateSecret = false;
|
||||||
bool _dataLoaded = false;
|
bool _dataLoaded = false;
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
List<int> _periodValues = [20, 30, 45, 60];
|
List<int> _periodValues = [20, 30, 45, 60];
|
||||||
@ -235,6 +233,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
|
|
||||||
final secret = _secretController.text.replaceAll(' ', '');
|
final secret = _secretController.text.replaceAll(' ', '');
|
||||||
final secretLengthValid = secret.length * 5 % 8 < 5;
|
final secretLengthValid = secret.length * 5 % 8 < 5;
|
||||||
|
final secretFormatValid = Format.base32.isValid(secret);
|
||||||
|
|
||||||
// is this credentials name/issuer pair different from all other?
|
// is this credentials name/issuer pair different from all other?
|
||||||
final isUnique = _credentials
|
final isUnique = _credentials
|
||||||
@ -271,7 +270,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void submit() async {
|
void submit() async {
|
||||||
if (secretLengthValid) {
|
if (secretLengthValid && secretFormatValid) {
|
||||||
final cred = CredentialData(
|
final cred = CredentialData(
|
||||||
issuer: issuerText.isEmpty ? null : issuerText,
|
issuer: issuerText.isEmpty ? null : issuerText,
|
||||||
name: nameText,
|
name: nameText,
|
||||||
@ -304,7 +303,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validateSecretLength = true;
|
_validateSecret = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,17 +364,17 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
limitBytesLength(issuerRemaining),
|
limitBytesLength(issuerRemaining),
|
||||||
],
|
],
|
||||||
buildCounter: buildByteCounterFor(issuerText),
|
buildCounter: buildByteCounterFor(issuerText),
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_issuer_optional,
|
labelText: l10n.s_issuer_optional,
|
||||||
helperText: '',
|
helperText:
|
||||||
// Prevents dialog resizing when disabled
|
'', // Prevents dialog resizing when disabled
|
||||||
prefixIcon: const Icon(Icons.business_outlined),
|
|
||||||
errorText: (byteLength(issuerText) > issuerMaxLength)
|
errorText: (byteLength(issuerText) > issuerMaxLength)
|
||||||
? '' // needs empty string to render as error
|
? '' // needs empty string to render as error
|
||||||
: issuerNoColon
|
: issuerNoColon
|
||||||
? null
|
? null
|
||||||
: l10n.l_invalid_character_issuer,
|
: l10n.l_invalid_character_issuer,
|
||||||
|
prefixIcon: const Icon(Icons.business_outlined),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -393,9 +392,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
maxLength: nameMaxLength,
|
maxLength: nameMaxLength,
|
||||||
buildCounter: buildByteCounterFor(nameText),
|
buildCounter: buildByteCounterFor(nameText),
|
||||||
inputFormatters: [limitBytesLength(nameRemaining)],
|
inputFormatters: [limitBytesLength(nameRemaining)],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
|
||||||
labelText: l10n.s_account_name,
|
labelText: l10n.s_account_name,
|
||||||
helperText: '',
|
helperText: '',
|
||||||
// Prevents dialog resizing when disabled
|
// Prevents dialog resizing when disabled
|
||||||
@ -404,6 +402,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
: isUnique
|
: isUnique
|
||||||
? null
|
? null
|
||||||
: l10n.l_name_already_exists,
|
: l10n.l_name_already_exists,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
suffixIcon:
|
||||||
|
(!isUnique || byteLength(nameText) > nameMaxLength)
|
||||||
|
? const Icon(Icons.error)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -423,18 +426,24 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
// would hint to use saved passwords for this field
|
// would hint to use saved passwords for this field
|
||||||
autofillHints:
|
autofillHints:
|
||||||
isAndroid ? [] : const [AutofillHints.password],
|
isAndroid ? [] : const [AutofillHints.password],
|
||||||
inputFormatters: <TextInputFormatter>[
|
decoration: AppInputDecoration(
|
||||||
FilteringTextInputFormatter.allow(
|
border: const OutlineInputBorder(),
|
||||||
_secretFormatterPattern)
|
labelText: l10n.s_secret_key,
|
||||||
],
|
errorText: _validateSecret && !secretLengthValid
|
||||||
decoration: InputDecoration(
|
? l10n.s_invalid_length
|
||||||
|
: _validateSecret && !secretFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.base32.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_isObscure
|
_isObscure
|
||||||
? Icons.visibility
|
? Icons.visibility
|
||||||
: Icons.visibility_off,
|
: Icons.visibility_off,
|
||||||
color: IconTheme.of(context).color,
|
color: !_validateSecret
|
||||||
),
|
? IconTheme.of(context).color
|
||||||
|
: null),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
@ -443,18 +452,12 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
tooltip: _isObscure
|
tooltip: _isObscure
|
||||||
? l10n.s_show_secret_key
|
? l10n.s_show_secret_key
|
||||||
: l10n.s_hide_secret_key,
|
: l10n.s_hide_secret_key,
|
||||||
),
|
)),
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
|
||||||
labelText: l10n.s_secret_key,
|
|
||||||
errorText: _validateSecretLength && !secretLengthValid
|
|
||||||
? l10n.s_invalid_length
|
|
||||||
: null),
|
|
||||||
readOnly: _dataLoaded,
|
readOnly: _dataLoaded,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validateSecretLength = false;
|
_validateSecret = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
|
@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/focus_utils.dart';
|
import '../../widgets/focus_utils.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
@ -42,6 +43,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
String _newPassword = '';
|
String _newPassword = '';
|
||||||
String _confirmPassword = '';
|
String _confirmPassword = '';
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
|
bool _isObscureCurrent = true;
|
||||||
|
bool _isObscureNew = true;
|
||||||
|
bool _isObscureConfirm = true;
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
FocusUtils.unfocus(context);
|
FocusUtils.unfocus(context);
|
||||||
@ -85,15 +89,28 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
Text(l10n.p_enter_current_password_or_reset),
|
Text(l10n.p_enter_current_password_or_reset),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscureCurrent,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.currentPasswordField,
|
key: keys.currentPasswordField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_password,
|
labelText: l10n.s_current_password,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
errorText: _currentIsWrong ? l10n.s_wrong_password : null,
|
||||||
errorText: _currentIsWrong ? l10n.s_wrong_password : null,
|
errorMaxLines: 3,
|
||||||
errorMaxLines: 3),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureCurrent
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureCurrent = !_isObscureCurrent;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscureCurrent
|
||||||
|
? l10n.s_show_password
|
||||||
|
: l10n.s_hide_password),
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -145,12 +162,24 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
AppTextField(
|
AppTextField(
|
||||||
key: keys.newPasswordField,
|
key: keys.newPasswordField,
|
||||||
autofocus: !widget.state.hasKey,
|
autofocus: !widget.state.hasKey,
|
||||||
obscureText: true,
|
obscureText: _isObscureNew,
|
||||||
autofillHints: const [AutofillHints.newPassword],
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_new_password,
|
labelText: l10n.s_new_password,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureNew
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureNew = !_isObscureNew;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscureNew
|
||||||
|
? l10n.s_show_password
|
||||||
|
: l10n.s_hide_password),
|
||||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
@ -167,12 +196,24 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
),
|
),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
key: keys.confirmPasswordField,
|
key: keys.confirmPasswordField,
|
||||||
obscureText: true,
|
obscureText: _isObscureConfirm,
|
||||||
autofillHints: const [AutofillHints.newPassword],
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_confirm_password,
|
labelText: l10n.s_confirm_password,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureConfirm
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureConfirm = !_isObscureConfirm;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscureConfirm
|
||||||
|
? l10n.s_show_password
|
||||||
|
: l10n.s_hide_password),
|
||||||
enabled:
|
enabled:
|
||||||
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
||||||
_newPassword.isNotEmpty,
|
_newPassword.isNotEmpty,
|
||||||
|
@ -26,6 +26,7 @@ import '../../app/views/app_page.dart';
|
|||||||
import '../../app/views/graphics.dart';
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../features.dart' as features;
|
import '../features.dart' as features;
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -161,7 +162,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
// Use the default style, but with a smaller font size:
|
// Use the default style, but with a smaller font size:
|
||||||
style: textTheme.titleMedium
|
style: textTheme.titleMedium
|
||||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
hintText: l10n.s_search_accounts,
|
hintText: l10n.s_search_accounts,
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(32)),
|
borderRadius: BorderRadius.all(Radius.circular(32)),
|
||||||
|
@ -25,6 +25,7 @@ import '../../app/models.dart';
|
|||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/focus_utils.dart';
|
import '../../widgets/focus_utils.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
@ -179,7 +180,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
buildCounter: buildByteCounterFor(_issuer),
|
buildCounter: buildByteCounterFor(_issuer),
|
||||||
inputFormatters: [limitBytesLength(issuerRemaining)],
|
inputFormatters: [limitBytesLength(issuerRemaining)],
|
||||||
key: keys.issuerField,
|
key: keys.issuerField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_issuer_optional,
|
labelText: l10n.s_issuer_optional,
|
||||||
helperText: '', // Prevents dialog resizing when disabled
|
helperText: '', // Prevents dialog resizing when disabled
|
||||||
@ -198,7 +199,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
inputFormatters: [limitBytesLength(nameRemaining)],
|
inputFormatters: [limitBytesLength(nameRemaining)],
|
||||||
buildCounter: buildByteCounterFor(_name),
|
buildCounter: buildByteCounterFor(_name),
|
||||||
key: keys.nameField,
|
key: keys.nameField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_account_name,
|
labelText: l10n.s_account_name,
|
||||||
helperText: '', // Prevents dialog resizing when disabled
|
helperText: '', // Prevents dialog resizing when disabled
|
||||||
|
@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -79,7 +80,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: _isObscure,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_password,
|
labelText: l10n.s_password,
|
||||||
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
|
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
|
||||||
@ -87,9 +88,10 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||||
color: IconTheme.of(context).color,
|
color: !_passwordIsWrong
|
||||||
),
|
? IconTheme.of(context).color
|
||||||
|
: null),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
@ -105,37 +107,48 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
}), // Update state on change
|
}), // Update state on change
|
||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 8.0),
|
||||||
),
|
Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
keystoreFailed
|
children: [
|
||||||
? ListTile(
|
Wrap(
|
||||||
leading: const Icon(Icons.warning_amber_rounded),
|
alignment: WrapAlignment.spaceBetween,
|
||||||
title: Text(l10n.l_keystore_unavailable),
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
dense: true,
|
spacing: 4.0,
|
||||||
minLeadingWidth: 0,
|
runSpacing: 8.0,
|
||||||
)
|
children: [
|
||||||
: CheckboxListTile(
|
keystoreFailed
|
||||||
title: Text(l10n.s_remember_password),
|
? Wrap(
|
||||||
dense: true,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
spacing: 4.0,
|
||||||
value: _remember,
|
runSpacing: 8.0,
|
||||||
onChanged: (value) {
|
children: [
|
||||||
setState(() {
|
const Icon(Icons.warning_amber_rounded),
|
||||||
_remember = value ?? false;
|
Text(l10n.l_keystore_unavailable)
|
||||||
});
|
],
|
||||||
},
|
)
|
||||||
|
: FilterChip(
|
||||||
|
label: Text(l10n.s_remember_password),
|
||||||
|
selected: _remember,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_remember = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
key: keys.unlockButton,
|
||||||
|
label: Text(l10n.s_unlock),
|
||||||
|
icon: const Icon(Icons.lock_open),
|
||||||
|
onPressed: _passwordController.text.isNotEmpty
|
||||||
|
? _submit
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
key: keys.unlockButton,
|
|
||||||
label: Text(l10n.s_unlock),
|
|
||||||
icon: const Icon(Icons.lock_open),
|
|
||||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
30
lib/otp/features.dart
Normal file
30
lib/otp/features.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../app/features.dart';
|
||||||
|
|
||||||
|
final actions = otp.feature('actions');
|
||||||
|
|
||||||
|
final actionsSwap = actions.feature('swap');
|
||||||
|
|
||||||
|
final slots = otp.feature('slots');
|
||||||
|
|
||||||
|
final slotsConfigureChalResp = slots.feature('configureChalResp');
|
||||||
|
final slotsConfigureHotp = slots.feature('configureHotp');
|
||||||
|
final slotsConfigureStatic = slots.feature('configureSlots');
|
||||||
|
final slotsConfigureYubiOtp = slots.feature('configureYubiOtp');
|
||||||
|
|
||||||
|
final slotsDelete = slots.feature('delete');
|
38
lib/otp/keys.dart
Normal file
38
lib/otp/keys.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022-2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const _prefix = 'otp.keys';
|
||||||
|
const _keyAction = '$_prefix.actions';
|
||||||
|
const _slotAction = '$_prefix.slot.actions';
|
||||||
|
|
||||||
|
// Key actions
|
||||||
|
const swapSlots = Key('$_keyAction.swap_slots');
|
||||||
|
|
||||||
|
// Slot actions
|
||||||
|
const configureYubiOtp = Key('$_slotAction.configure_yubiotp');
|
||||||
|
const configureHotp = Key('$_slotAction.configure_hotp');
|
||||||
|
const configureStatic = Key('$_slotAction.configure_static');
|
||||||
|
const configureChalResp = Key('$_slotAction.configure_chal_resp');
|
||||||
|
const deleteAction = Key('$_slotAction.delete');
|
||||||
|
|
||||||
|
const saveButton = Key('$_prefix.save');
|
||||||
|
const deleteButton = Key('$_prefix.delete');
|
||||||
|
|
||||||
|
const secretField = Key('$_prefix.secret');
|
||||||
|
const publicIdField = Key('$_prefix.public_id');
|
||||||
|
const privateIdField = Key('$_prefix.private_id');
|
113
lib/otp/models.dart
Normal file
113
lib/otp/models.dart
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'models.freezed.dart';
|
||||||
|
part 'models.g.dart';
|
||||||
|
|
||||||
|
enum SlotId {
|
||||||
|
one('one', 1),
|
||||||
|
two('two', 2);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final int numberId;
|
||||||
|
const SlotId(this.id, this.numberId);
|
||||||
|
|
||||||
|
String getDisplayName(AppLocalizations l10n) {
|
||||||
|
return switch (this) {
|
||||||
|
SlotId.one => l10n.s_otp_slot_one,
|
||||||
|
SlotId.two => l10n.s_otp_slot_two
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SlotId.fromJson(String value) =>
|
||||||
|
SlotId.values.firstWhere((e) => e.id == value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class OtpState with _$OtpState {
|
||||||
|
const OtpState._();
|
||||||
|
factory OtpState({
|
||||||
|
required bool slot1Configured,
|
||||||
|
required bool slot2Configured,
|
||||||
|
}) = _OtpState;
|
||||||
|
|
||||||
|
factory OtpState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OtpStateFromJson(json);
|
||||||
|
|
||||||
|
List<OtpSlot> get slots => [
|
||||||
|
OtpSlot(slot: SlotId.one, isConfigured: slot1Configured),
|
||||||
|
OtpSlot(slot: SlotId.two, isConfigured: slot2Configured),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class OtpSlot with _$OtpSlot {
|
||||||
|
factory OtpSlot({required SlotId slot, required bool isConfigured}) =
|
||||||
|
_OtpSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SlotConfigurationOptions with _$SlotConfigurationOptions {
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonSerializable(includeIfNull: false)
|
||||||
|
factory SlotConfigurationOptions(
|
||||||
|
{bool? digits8,
|
||||||
|
bool? requireTouch,
|
||||||
|
bool? appendCr}) = _SlotConfigurationOptions;
|
||||||
|
|
||||||
|
factory SlotConfigurationOptions.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationOptionsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.snake)
|
||||||
|
class SlotConfiguration with _$SlotConfiguration {
|
||||||
|
const SlotConfiguration._();
|
||||||
|
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonSerializable(explicitToJson: true, includeIfNull: false)
|
||||||
|
const factory SlotConfiguration.hotp(
|
||||||
|
{required String key,
|
||||||
|
SlotConfigurationOptions? options}) = _SlotConfigurationHotp;
|
||||||
|
|
||||||
|
@FreezedUnionValue('hmac_sha1')
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonSerializable(explicitToJson: true, includeIfNull: false)
|
||||||
|
const factory SlotConfiguration.chalresp(
|
||||||
|
{required String key,
|
||||||
|
SlotConfigurationOptions? options}) = _SlotConfigurationHmacSha1;
|
||||||
|
|
||||||
|
@FreezedUnionValue('static_password')
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonSerializable(explicitToJson: true, includeIfNull: false)
|
||||||
|
const factory SlotConfiguration.static(
|
||||||
|
{required String password,
|
||||||
|
required String keyboardLayout,
|
||||||
|
SlotConfigurationOptions? options}) = _SlotConfigurationStaticPassword;
|
||||||
|
|
||||||
|
// ignore: invalid_annotation_target
|
||||||
|
@JsonSerializable(explicitToJson: true, includeIfNull: false)
|
||||||
|
const factory SlotConfiguration.yubiotp(
|
||||||
|
{required String publicId,
|
||||||
|
required String privateId,
|
||||||
|
required String key,
|
||||||
|
SlotConfigurationOptions? options}) = _SlotConfigurationYubiOtp;
|
||||||
|
|
||||||
|
factory SlotConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationFromJson(json);
|
||||||
|
}
|
1491
lib/otp/models.freezed.dart
Normal file
1491
lib/otp/models.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
161
lib/otp/models.g.dart
Normal file
161
lib/otp/models.g.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'models.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$OtpStateImpl _$$OtpStateImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OtpStateImpl(
|
||||||
|
slot1Configured: json['slot1_configured'] as bool,
|
||||||
|
slot2Configured: json['slot2_configured'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$OtpStateImplToJson(_$OtpStateImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'slot1_configured': instance.slot1Configured,
|
||||||
|
'slot2_configured': instance.slot2Configured,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SlotConfigurationOptionsImpl _$$SlotConfigurationOptionsImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationOptionsImpl(
|
||||||
|
digits8: json['digits8'] as bool?,
|
||||||
|
requireTouch: json['require_touch'] as bool?,
|
||||||
|
appendCr: json['append_cr'] as bool?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SlotConfigurationOptionsImplToJson(
|
||||||
|
_$SlotConfigurationOptionsImpl instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('digits8', instance.digits8);
|
||||||
|
writeNotNull('require_touch', instance.requireTouch);
|
||||||
|
writeNotNull('append_cr', instance.appendCr);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
_$SlotConfigurationHotpImpl _$$SlotConfigurationHotpImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationHotpImpl(
|
||||||
|
key: json['key'] as String,
|
||||||
|
options: json['options'] == null
|
||||||
|
? null
|
||||||
|
: SlotConfigurationOptions.fromJson(
|
||||||
|
json['options'] as Map<String, dynamic>),
|
||||||
|
$type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SlotConfigurationHotpImplToJson(
|
||||||
|
_$SlotConfigurationHotpImpl instance) {
|
||||||
|
final val = <String, dynamic>{
|
||||||
|
'key': instance.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('options', instance.options?.toJson());
|
||||||
|
val['type'] = instance.$type;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
_$SlotConfigurationHmacSha1Impl _$$SlotConfigurationHmacSha1ImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationHmacSha1Impl(
|
||||||
|
key: json['key'] as String,
|
||||||
|
options: json['options'] == null
|
||||||
|
? null
|
||||||
|
: SlotConfigurationOptions.fromJson(
|
||||||
|
json['options'] as Map<String, dynamic>),
|
||||||
|
$type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SlotConfigurationHmacSha1ImplToJson(
|
||||||
|
_$SlotConfigurationHmacSha1Impl instance) {
|
||||||
|
final val = <String, dynamic>{
|
||||||
|
'key': instance.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('options', instance.options?.toJson());
|
||||||
|
val['type'] = instance.$type;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
_$SlotConfigurationStaticPasswordImpl
|
||||||
|
_$$SlotConfigurationStaticPasswordImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationStaticPasswordImpl(
|
||||||
|
password: json['password'] as String,
|
||||||
|
keyboardLayout: json['keyboard_layout'] as String,
|
||||||
|
options: json['options'] == null
|
||||||
|
? null
|
||||||
|
: SlotConfigurationOptions.fromJson(
|
||||||
|
json['options'] as Map<String, dynamic>),
|
||||||
|
$type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SlotConfigurationStaticPasswordImplToJson(
|
||||||
|
_$SlotConfigurationStaticPasswordImpl instance) {
|
||||||
|
final val = <String, dynamic>{
|
||||||
|
'password': instance.password,
|
||||||
|
'keyboard_layout': instance.keyboardLayout,
|
||||||
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('options', instance.options?.toJson());
|
||||||
|
val['type'] = instance.$type;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
_$SlotConfigurationYubiOtpImpl _$$SlotConfigurationYubiOtpImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SlotConfigurationYubiOtpImpl(
|
||||||
|
publicId: json['public_id'] as String,
|
||||||
|
privateId: json['private_id'] as String,
|
||||||
|
key: json['key'] as String,
|
||||||
|
options: json['options'] == null
|
||||||
|
? null
|
||||||
|
: SlotConfigurationOptions.fromJson(
|
||||||
|
json['options'] as Map<String, dynamic>),
|
||||||
|
$type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SlotConfigurationYubiOtpImplToJson(
|
||||||
|
_$SlotConfigurationYubiOtpImpl instance) {
|
||||||
|
final val = <String, dynamic>{
|
||||||
|
'public_id': instance.publicId,
|
||||||
|
'private_id': instance.privateId,
|
||||||
|
'key': instance.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('options', instance.options?.toJson());
|
||||||
|
val['type'] = instance.$type;
|
||||||
|
return val;
|
||||||
|
}
|
52
lib/otp/state.dart
Normal file
52
lib/otp/state.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../app/models.dart';
|
||||||
|
import '../core/state.dart';
|
||||||
|
import 'models.dart';
|
||||||
|
|
||||||
|
final yubiOtpOutputProvider =
|
||||||
|
StateNotifierProvider<YubiOtpOutputNotifier, File?>(
|
||||||
|
(ref) => YubiOtpOutputNotifier());
|
||||||
|
|
||||||
|
class YubiOtpOutputNotifier extends StateNotifier<File?> {
|
||||||
|
YubiOtpOutputNotifier() : super(null);
|
||||||
|
|
||||||
|
void setOutput(File? file) {
|
||||||
|
state = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final otpStateProvider = AsyncNotifierProvider.autoDispose
|
||||||
|
.family<OtpStateNotifier, OtpState, DevicePath>(
|
||||||
|
() => throw UnimplementedError(),
|
||||||
|
);
|
||||||
|
|
||||||
|
abstract class OtpStateNotifier extends ApplicationStateNotifier<OtpState> {
|
||||||
|
Future<String> generateStaticPassword(int length, String layout);
|
||||||
|
Future<String> modhexEncodeSerial(int serial);
|
||||||
|
Future<Map<String, List<String>>> getKeyboardLayouts();
|
||||||
|
Future<String> formatYubiOtpCsv(
|
||||||
|
int serial, String publicId, String privateId, String key);
|
||||||
|
Future<void> swapSlots();
|
||||||
|
Future<void> configureSlot(SlotId slot,
|
||||||
|
{required SlotConfiguration configuration});
|
||||||
|
Future<void> deleteSlot(SlotId slot);
|
||||||
|
}
|
175
lib/otp/views/actions.dart
Normal file
175
lib/otp/views/actions.dart
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/shortcuts.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'configure_chalresp_dialog.dart';
|
||||||
|
import 'configure_hotp_dialog.dart';
|
||||||
|
import 'configure_static_dialog.dart';
|
||||||
|
import 'configure_yubiotp_dialog.dart';
|
||||||
|
import 'delete_slot_dialog.dart';
|
||||||
|
|
||||||
|
class ConfigureChalRespIntent extends Intent {
|
||||||
|
const ConfigureChalRespIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigureHotpIntent extends Intent {
|
||||||
|
const ConfigureHotpIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigureStaticIntent extends Intent {
|
||||||
|
const ConfigureStaticIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigureYubiOtpIntent extends Intent {
|
||||||
|
const ConfigureYubiOtpIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget registerOtpActions(
|
||||||
|
DevicePath devicePath,
|
||||||
|
OtpSlot otpSlot, {
|
||||||
|
required WidgetRef ref,
|
||||||
|
required Widget Function(BuildContext context) builder,
|
||||||
|
Map<Type, Action<Intent>> actions = const {},
|
||||||
|
}) {
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
return Actions(
|
||||||
|
actions: {
|
||||||
|
if (hasFeature(features.slotsConfigureChalResp))
|
||||||
|
ConfigureChalRespIntent:
|
||||||
|
CallbackAction<ConfigureChalRespIntent>(onInvoke: (intent) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
await withContext((context) async {
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ConfigureChalrespDialog(devicePath, otpSlot));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.slotsConfigureHotp))
|
||||||
|
ConfigureHotpIntent:
|
||||||
|
CallbackAction<ConfigureHotpIntent>(onInvoke: (intent) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
await withContext((context) async {
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfigureHotpDialog(devicePath, otpSlot));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.slotsConfigureStatic))
|
||||||
|
ConfigureStaticIntent:
|
||||||
|
CallbackAction<ConfigureStaticIntent>(onInvoke: (intent) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
final keyboardLayouts = await ref
|
||||||
|
.read(otpStateProvider(devicePath).notifier)
|
||||||
|
.getKeyboardLayouts();
|
||||||
|
await withContext((context) async {
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfigureStaticDialog(
|
||||||
|
devicePath, otpSlot, keyboardLayouts));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.slotsConfigureYubiOtp))
|
||||||
|
ConfigureYubiOtpIntent:
|
||||||
|
CallbackAction<ConfigureYubiOtpIntent>(onInvoke: (intent) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
await withContext((context) async {
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ConfigureYubiOtpDialog(devicePath, otpSlot));
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.slotsDelete))
|
||||||
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
final bool? deleted = await withContext((context) async =>
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
DeleteSlotDialog(devicePath, otpSlot)) ??
|
||||||
|
false);
|
||||||
|
return deleted;
|
||||||
|
}),
|
||||||
|
...actions,
|
||||||
|
},
|
||||||
|
child: Builder(builder: builder),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActionItem> buildSlotActions(bool isConfigured, AppLocalizations l10n) {
|
||||||
|
return [
|
||||||
|
ActionItem(
|
||||||
|
key: keys.configureYubiOtp,
|
||||||
|
feature: features.slotsConfigureYubiOtp,
|
||||||
|
icon: const Icon(Icons.shuffle_outlined),
|
||||||
|
title: l10n.s_yubiotp,
|
||||||
|
subtitle: l10n.l_yubiotp_desc,
|
||||||
|
intent: const ConfigureYubiOtpIntent(),
|
||||||
|
),
|
||||||
|
ActionItem(
|
||||||
|
key: keys.configureChalResp,
|
||||||
|
feature: features.slotsConfigureChalResp,
|
||||||
|
icon: const Icon(Icons.key_outlined),
|
||||||
|
title: l10n.s_challenge_response,
|
||||||
|
subtitle: l10n.l_challenge_response_desc,
|
||||||
|
intent: const ConfigureChalRespIntent()),
|
||||||
|
ActionItem(
|
||||||
|
key: keys.configureStatic,
|
||||||
|
feature: features.slotsConfigureStatic,
|
||||||
|
icon: const Icon(Icons.password_outlined),
|
||||||
|
title: l10n.s_static_password,
|
||||||
|
subtitle: l10n.l_static_password_desc,
|
||||||
|
intent: const ConfigureStaticIntent()),
|
||||||
|
ActionItem(
|
||||||
|
key: keys.configureHotp,
|
||||||
|
feature: features.slotsConfigureHotp,
|
||||||
|
icon: const Icon(Icons.tag_outlined),
|
||||||
|
title: l10n.s_hotp,
|
||||||
|
subtitle: l10n.l_hotp_desc,
|
||||||
|
intent: const ConfigureHotpIntent()),
|
||||||
|
ActionItem(
|
||||||
|
key: keys.deleteAction,
|
||||||
|
feature: features.slotsDelete,
|
||||||
|
actionStyle: ActionStyle.error,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
title: l10n.s_delete_slot,
|
||||||
|
subtitle: l10n.l_delete_slot_desc,
|
||||||
|
intent: isConfigured ? const DeleteIntent() : null,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
187
lib/otp/views/configure_chalresp_dialog.dart
Normal file
187
lib/otp/views/configure_chalresp_dialog.dart
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
|
import '../../widgets/app_text_field.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
|
final _log = Logger('otp.view.configure_chalresp_dialog');
|
||||||
|
|
||||||
|
class ConfigureChalrespDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
const ConfigureChalrespDialog(this.devicePath, this.otpSlot, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_ConfigureChalrespDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfigureChalrespDialogState
|
||||||
|
extends ConsumerState<ConfigureChalrespDialog> {
|
||||||
|
final _secretController = TextEditingController();
|
||||||
|
bool _validateSecret = false;
|
||||||
|
bool _requireTouch = false;
|
||||||
|
final int secretMaxLength = 40;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_secretController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final secret = _secretController.text;
|
||||||
|
final secretLengthValid = secret.isNotEmpty &&
|
||||||
|
secret.length % 2 == 0 &&
|
||||||
|
secret.length <= secretMaxLength;
|
||||||
|
final secretFormatValid = Format.hex.isValid(secret);
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_challenge_response),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.saveButton,
|
||||||
|
onPressed: !_validateSecret
|
||||||
|
? () async {
|
||||||
|
if (!secretLengthValid || !secretFormatValid) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecret = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final otpNotifier =
|
||||||
|
ref.read(otpStateProvider(widget.devicePath).notifier);
|
||||||
|
try {
|
||||||
|
await otpNotifier.configureSlot(widget.otpSlot.slot,
|
||||||
|
configuration: SlotConfiguration.chalresp(
|
||||||
|
key: secret,
|
||||||
|
options: SlotConfigurationOptions(
|
||||||
|
requireTouch: _requireTouch)));
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.l_slot_credential_configured(
|
||||||
|
l10n.s_challenge_response));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_log.error('Failed to program credential', e);
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.p_otp_slot_configuration_error(
|
||||||
|
widget.otpSlot.slot.getDisplayName(l10n)),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_save),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AppTextField(
|
||||||
|
key: keys.secretField,
|
||||||
|
autofocus: true,
|
||||||
|
controller: _secretController,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
maxLength: secretMaxLength,
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_secret_key,
|
||||||
|
errorText: _validateSecret && !secretLengthValid
|
||||||
|
? l10n.s_invalid_length
|
||||||
|
: _validateSecret && !secretFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
final random = Random.secure();
|
||||||
|
final key = List.generate(
|
||||||
|
20,
|
||||||
|
(_) => random
|
||||||
|
.nextInt(256)
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')).join();
|
||||||
|
setState(() {
|
||||||
|
_secretController.text = key;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: l10n.s_generate_random,
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecret = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_require_touch),
|
||||||
|
selected: _requireTouch,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_requireTouch = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
200
lib/otp/views/configure_hotp_dialog.dart
Normal file
200
lib/otp/views/configure_hotp_dialog.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../oath/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
|
import '../../widgets/app_text_field.dart';
|
||||||
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
|
final _log = Logger('otp.view.configure_hotp_dialog');
|
||||||
|
|
||||||
|
class ConfigureHotpDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
const ConfigureHotpDialog(this.devicePath, this.otpSlot, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_ConfigureHotpDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||||
|
final _secretController = TextEditingController();
|
||||||
|
bool _validateSecret = false;
|
||||||
|
int _digits = defaultDigits;
|
||||||
|
final List<int> _digitsValues = [6, 8];
|
||||||
|
bool _appendEnter = true;
|
||||||
|
bool _isObscure = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_secretController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final secret = _secretController.text.replaceAll(' ', '');
|
||||||
|
final secretLengthValid = secret.isNotEmpty && secret.length * 5 % 8 < 5;
|
||||||
|
final secretFormatValid = Format.base32.isValid(secret);
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_hotp),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.saveButton,
|
||||||
|
onPressed: !_validateSecret
|
||||||
|
? () async {
|
||||||
|
if (!secretLengthValid || !secretFormatValid) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecret = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final otpNotifier =
|
||||||
|
ref.read(otpStateProvider(widget.devicePath).notifier);
|
||||||
|
try {
|
||||||
|
await otpNotifier.configureSlot(widget.otpSlot.slot,
|
||||||
|
configuration: SlotConfiguration.hotp(
|
||||||
|
key: secret,
|
||||||
|
options: SlotConfigurationOptions(
|
||||||
|
digits8: _digits == 8,
|
||||||
|
appendCr: _appendEnter)));
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showMessage(context,
|
||||||
|
l10n.l_slot_credential_configured(l10n.s_hotp));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_log.error('Failed to program credential', e);
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.p_otp_slot_configuration_error(
|
||||||
|
widget.otpSlot.slot.getDisplayName(l10n)),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_save),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AppTextField(
|
||||||
|
key: keys.secretField,
|
||||||
|
controller: _secretController,
|
||||||
|
obscureText: _isObscure,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_secret_key,
|
||||||
|
helperText: '', // Prevents resizing when errorText shown
|
||||||
|
errorText: _validateSecret && !secretLengthValid
|
||||||
|
? l10n.s_invalid_length
|
||||||
|
: _validateSecret && !secretFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.base32.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||||
|
color: !_validateSecret
|
||||||
|
? IconTheme.of(context).color
|
||||||
|
: null),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscure = !_isObscure;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscure
|
||||||
|
? l10n.s_show_secret_key
|
||||||
|
: l10n.s_hide_secret_key,
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecret = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_append_enter),
|
||||||
|
tooltip: l10n.l_append_enter_desc,
|
||||||
|
selected: _appendEnter,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_appendEnter = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ChoiceFilterChip<int>(
|
||||||
|
items: _digitsValues,
|
||||||
|
value: _digits,
|
||||||
|
selected: _digits != defaultDigits,
|
||||||
|
itemBuilder: (value) => Text(l10n.s_num_digits(value)),
|
||||||
|
onChanged: (digits) {
|
||||||
|
setState(() {
|
||||||
|
_digits = digits;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
222
lib/otp/views/configure_static_dialog.dart
Normal file
222
lib/otp/views/configure_static_dialog.dart
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
|
import '../../widgets/app_text_field.dart';
|
||||||
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
|
final _log = Logger('otp.view.configure_static_dialog');
|
||||||
|
|
||||||
|
class ConfigureStaticDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
final Map<String, List<String>> keyboardLayouts;
|
||||||
|
const ConfigureStaticDialog(
|
||||||
|
this.devicePath, this.otpSlot, this.keyboardLayouts,
|
||||||
|
{super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_ConfigureStaticDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final passwordMaxLength = 38;
|
||||||
|
bool _validatePassword = false;
|
||||||
|
bool _appendEnter = true;
|
||||||
|
String _keyboardLayout = '';
|
||||||
|
String _defaultKeyboardLayout = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final modhexLayout = widget.keyboardLayouts.keys.toList()[0];
|
||||||
|
_keyboardLayout = modhexLayout;
|
||||||
|
_defaultKeyboardLayout = modhexLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegExp generateFormatterPattern(String layout) {
|
||||||
|
final allowedCharacters = widget.keyboardLayouts[layout] ?? [];
|
||||||
|
|
||||||
|
final pattern =
|
||||||
|
allowedCharacters.map((char) => RegExp.escape(char)).join('');
|
||||||
|
|
||||||
|
return RegExp('^[$pattern]+\$', caseSensitive: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final password = _passwordController.text.replaceAll(' ', '');
|
||||||
|
final passwordLengthValid =
|
||||||
|
password.isNotEmpty && password.length <= passwordMaxLength;
|
||||||
|
final passwordFormatValid =
|
||||||
|
generateFormatterPattern(_keyboardLayout).hasMatch(password);
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_static_password),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.saveButton,
|
||||||
|
onPressed: !_validatePassword
|
||||||
|
? () async {
|
||||||
|
if (!passwordLengthValid || !passwordFormatValid) {
|
||||||
|
setState(() {
|
||||||
|
_validatePassword = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final otpNotifier =
|
||||||
|
ref.read(otpStateProvider(widget.devicePath).notifier);
|
||||||
|
try {
|
||||||
|
await otpNotifier.configureSlot(widget.otpSlot.slot,
|
||||||
|
configuration: SlotConfiguration.static(
|
||||||
|
password: password,
|
||||||
|
keyboardLayout: _keyboardLayout,
|
||||||
|
options: SlotConfigurationOptions(
|
||||||
|
appendCr: _appendEnter)));
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.l_slot_credential_configured(
|
||||||
|
l10n.s_static_password));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_log.error('Failed to program credential', e);
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.p_otp_slot_configuration_error(
|
||||||
|
widget.otpSlot.slot.getDisplayName(l10n)),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_save),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AppTextField(
|
||||||
|
key: keys.secretField,
|
||||||
|
autofocus: true,
|
||||||
|
controller: _passwordController,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
maxLength: passwordMaxLength,
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_password,
|
||||||
|
errorText: _validatePassword && !passwordLengthValid
|
||||||
|
? l10n.s_invalid_length
|
||||||
|
: _validatePassword && !passwordFormatValid
|
||||||
|
? l10n.l_invalid_keyboard_character
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: l10n.s_generate_passowrd,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () async {
|
||||||
|
final password = await ref
|
||||||
|
.read(otpStateProvider(widget.devicePath).notifier)
|
||||||
|
.generateStaticPassword(
|
||||||
|
passwordMaxLength, _keyboardLayout);
|
||||||
|
setState(() {
|
||||||
|
_validatePassword = false;
|
||||||
|
_passwordController.text = password;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validatePassword = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_append_enter),
|
||||||
|
tooltip: l10n.l_append_enter_desc,
|
||||||
|
selected: _appendEnter,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_appendEnter = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ChoiceFilterChip(
|
||||||
|
items: widget.keyboardLayouts.keys.toList(),
|
||||||
|
value: _keyboardLayout,
|
||||||
|
selected: _keyboardLayout != _defaultKeyboardLayout,
|
||||||
|
labelBuilder: (value) => Text('Keyboard $value'),
|
||||||
|
itemBuilder: (value) => Text(value),
|
||||||
|
onChanged: (layout) {
|
||||||
|
setState(() {
|
||||||
|
_keyboardLayout = layout;
|
||||||
|
_validatePassword = false;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
375
lib/otp/views/configure_yubiotp_dialog.dart
Normal file
375
lib/otp/views/configure_yubiotp_dialog.dart
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
|
import '../../widgets/app_text_field.dart';
|
||||||
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
|
final _log = Logger('otp.view.configure_yubiotp_dialog');
|
||||||
|
|
||||||
|
enum OutputActions {
|
||||||
|
selectFile,
|
||||||
|
noOutput;
|
||||||
|
|
||||||
|
const OutputActions();
|
||||||
|
|
||||||
|
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||||
|
OutputActions.selectFile => l10n.l_select_file,
|
||||||
|
OutputActions.noOutput => l10n.l_no_export_file
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigureYubiOtpDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
const ConfigureYubiOtpDialog(this.devicePath, this.otpSlot, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||||
|
_ConfigureYubiOtpDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfigureYubiOtpDialogState
|
||||||
|
extends ConsumerState<ConfigureYubiOtpDialog> {
|
||||||
|
final _secretController = TextEditingController();
|
||||||
|
final _publicIdController = TextEditingController();
|
||||||
|
final _privateIdController = TextEditingController();
|
||||||
|
OutputActions _action = OutputActions.noOutput;
|
||||||
|
bool _appendEnter = true;
|
||||||
|
bool _validateSecretFormat = false;
|
||||||
|
bool _validatePublicIdFormat = false;
|
||||||
|
bool _validatePrivateIdFormat = false;
|
||||||
|
final secretLength = 32;
|
||||||
|
final publicIdLength = 12;
|
||||||
|
final privateIdLength = 12;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_secretController.dispose();
|
||||||
|
_publicIdController.dispose();
|
||||||
|
_privateIdController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final info = ref.watch(currentDeviceDataProvider).valueOrNull?.info;
|
||||||
|
|
||||||
|
final secret = _secretController.text;
|
||||||
|
final secretLengthValid = secret.length == secretLength;
|
||||||
|
final secretFormatValid = Format.hex.isValid(secret);
|
||||||
|
|
||||||
|
final privateId = _privateIdController.text;
|
||||||
|
final privateIdLengthValid = privateId.length == privateIdLength;
|
||||||
|
final privatedIdFormatValid = Format.hex.isValid(privateId);
|
||||||
|
|
||||||
|
final publicId = _publicIdController.text;
|
||||||
|
final publicIdLengthValid = publicId.length == publicIdLength;
|
||||||
|
final publicIdFormatValid = Format.modhex.isValid(publicId);
|
||||||
|
|
||||||
|
final lengthsValid =
|
||||||
|
secretLengthValid && privateIdLengthValid && publicIdLengthValid;
|
||||||
|
|
||||||
|
final outputFile = ref.read(yubiOtpOutputProvider);
|
||||||
|
|
||||||
|
Future<bool> selectFile() async {
|
||||||
|
final filePath = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: l10n.l_export_configuration_file,
|
||||||
|
allowedExtensions: ['csv'],
|
||||||
|
type: FileType.custom,
|
||||||
|
lockParentWindow: true);
|
||||||
|
|
||||||
|
if (filePath == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(yubiOtpOutputProvider.notifier).setOutput(File(filePath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_yubiotp),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.saveButton,
|
||||||
|
onPressed: lengthsValid
|
||||||
|
? () async {
|
||||||
|
if (!secretFormatValid ||
|
||||||
|
!publicIdFormatValid ||
|
||||||
|
!privatedIdFormatValid) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecretFormat = !secretFormatValid;
|
||||||
|
_validatePublicIdFormat = !publicIdFormatValid;
|
||||||
|
_validatePrivateIdFormat = !privatedIdFormatValid;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final otpNotifier =
|
||||||
|
ref.read(otpStateProvider(widget.devicePath).notifier);
|
||||||
|
try {
|
||||||
|
await otpNotifier.configureSlot(widget.otpSlot.slot,
|
||||||
|
configuration: SlotConfiguration.yubiotp(
|
||||||
|
publicId: publicId,
|
||||||
|
privateId: privateId,
|
||||||
|
key: secret,
|
||||||
|
options: SlotConfigurationOptions(
|
||||||
|
appendCr: _appendEnter)));
|
||||||
|
if (outputFile != null) {
|
||||||
|
final csv = await otpNotifier.formatYubiOtpCsv(
|
||||||
|
info!.serial!, publicId, privateId, secret);
|
||||||
|
|
||||||
|
await outputFile.writeAsString(
|
||||||
|
'$csv${Platform.lineTerminator}',
|
||||||
|
mode: FileMode.append);
|
||||||
|
}
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
outputFile != null
|
||||||
|
? l10n.l_slot_credential_configured_and_exported(
|
||||||
|
l10n.s_yubiotp,
|
||||||
|
outputFile.uri.pathSegments.last)
|
||||||
|
: l10n.l_slot_credential_configured(
|
||||||
|
l10n.s_yubiotp));
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_log.error('Failed to program credential', e);
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
final String errorMessage;
|
||||||
|
if (e is PathNotFoundException) {
|
||||||
|
errorMessage = '${e.message} ${e.path.toString()}';
|
||||||
|
} else {
|
||||||
|
errorMessage = l10n.p_otp_slot_configuration_error(
|
||||||
|
widget.otpSlot.slot.getDisplayName(l10n));
|
||||||
|
}
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
errorMessage,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(l10n.s_save),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AppTextField(
|
||||||
|
key: keys.publicIdField,
|
||||||
|
autofocus: true,
|
||||||
|
controller: _publicIdController,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
maxLength: publicIdLength,
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_public_id,
|
||||||
|
errorText: _validatePublicIdFormat && !publicIdFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.modhex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.public_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: l10n.s_use_serial,
|
||||||
|
icon: const Icon(Icons.auto_awesome_outlined),
|
||||||
|
onPressed: (info?.serial != null)
|
||||||
|
? () async {
|
||||||
|
final publicId = await ref
|
||||||
|
.read(otpStateProvider(widget.devicePath)
|
||||||
|
.notifier)
|
||||||
|
.modhexEncodeSerial(info!.serial!);
|
||||||
|
setState(() {
|
||||||
|
_publicIdController.text = publicId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validatePublicIdFormat = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
key: keys.privateIdField,
|
||||||
|
controller: _privateIdController,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
maxLength: privateIdLength,
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_private_id,
|
||||||
|
errorText: _validatePrivateIdFormat && !privatedIdFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: l10n.s_generate_random,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
final random = Random.secure();
|
||||||
|
final key = List.generate(
|
||||||
|
6,
|
||||||
|
(_) => random
|
||||||
|
.nextInt(256)
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')).join();
|
||||||
|
setState(() {
|
||||||
|
_privateIdController.text = key;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validatePrivateIdFormat = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
key: keys.secretField,
|
||||||
|
controller: _secretController,
|
||||||
|
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||||
|
maxLength: secretLength,
|
||||||
|
decoration: AppInputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: l10n.s_secret_key,
|
||||||
|
errorText: _validateSecretFormat && !secretFormatValid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: l10n.s_generate_random,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
final random = Random.secure();
|
||||||
|
final key = List.generate(
|
||||||
|
16,
|
||||||
|
(_) => random
|
||||||
|
.nextInt(256)
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')).join();
|
||||||
|
setState(() {
|
||||||
|
_secretController.text = key;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_validateSecretFormat = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 4.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
FilterChip(
|
||||||
|
label: Text(l10n.s_append_enter),
|
||||||
|
tooltip: l10n.l_append_enter_desc,
|
||||||
|
selected: _appendEnter,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_appendEnter = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ChoiceFilterChip<OutputActions>(
|
||||||
|
tooltip: outputFile?.path ?? l10n.s_no_export,
|
||||||
|
selected: outputFile != null,
|
||||||
|
avatar: outputFile != null
|
||||||
|
? Icon(Icons.check,
|
||||||
|
color: Theme.of(context).colorScheme.secondary)
|
||||||
|
: null,
|
||||||
|
value: _action,
|
||||||
|
items: OutputActions.values,
|
||||||
|
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
|
||||||
|
labelBuilder: (_) {
|
||||||
|
String? fileName = outputFile?.uri.pathSegments.last;
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 140),
|
||||||
|
child: Text(
|
||||||
|
fileName != null
|
||||||
|
? '${l10n.s_export} $fileName'
|
||||||
|
: _action.getDisplayName(l10n),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == OutputActions.noOutput) {
|
||||||
|
ref.read(yubiOtpOutputProvider.notifier).setOutput(null);
|
||||||
|
setState(() {
|
||||||
|
_action = value;
|
||||||
|
});
|
||||||
|
} else if (value == OutputActions.selectFile) {
|
||||||
|
if (await selectFile()) {
|
||||||
|
setState(() {
|
||||||
|
_action = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
83
lib/otp/views/delete_slot_dialog.dart
Normal file
83
lib/otp/views/delete_slot_dialog.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
|
||||||
|
class DeleteSlotDialog extends ConsumerWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
const DeleteSlotDialog(this.devicePath, this.otpSlot, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_delete_slot),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
key: keys.deleteButton,
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(otpStateProvider(devicePath).notifier)
|
||||||
|
.deleteSlot(otpSlot.slot);
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
showMessage(context, l10n.l_slot_deleted);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
showMessage(
|
||||||
|
context,
|
||||||
|
l10n.p_otp_slot_configuration_error(
|
||||||
|
otpSlot.slot.getDisplayName(l10n)),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(l10n.s_delete),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n
|
||||||
|
.p_warning_delete_slot_configuration(otpSlot.slot.numberId)),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
57
lib/otp/views/key_actions.dart
Normal file
57
lib/otp/views/key_actions.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/views/action_list.dart';
|
||||||
|
import '../../app/views/fs_dialog.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
|
import '../keys.dart' as keys;
|
||||||
|
import '../models.dart';
|
||||||
|
import 'swap_slots_dialog.dart';
|
||||||
|
|
||||||
|
Widget otpBuildActions(BuildContext context, DevicePath devicePath,
|
||||||
|
OtpState otpState, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
return FsDialog(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ActionListSection(l10n.s_manage, children: [
|
||||||
|
ActionListItem(
|
||||||
|
key: keys.swapSlots,
|
||||||
|
feature: features.actionsSwap,
|
||||||
|
title: l10n.s_swap_slots,
|
||||||
|
subtitle: l10n.l_swap_slots_desc,
|
||||||
|
icon: const Icon(Icons.swap_vert_outlined),
|
||||||
|
onTap: (otpState.slot1Configured || otpState.slot2Configured)
|
||||||
|
? (context) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SwapSlotsDialog(devicePath));
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
113
lib/otp/views/otp_screen.dart
Normal file
113
lib/otp/views/otp_screen.dart
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/shortcuts.dart';
|
||||||
|
import '../../app/views/app_failure_page.dart';
|
||||||
|
import '../../app/views/app_list_item.dart';
|
||||||
|
import '../../app/views/app_page.dart';
|
||||||
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../../widgets/list_title.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'actions.dart';
|
||||||
|
import 'key_actions.dart';
|
||||||
|
import 'slot_dialog.dart';
|
||||||
|
|
||||||
|
class OtpScreen extends ConsumerWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
|
||||||
|
const OtpScreen(this.devicePath, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
return ref.watch(otpStateProvider(devicePath)).when(
|
||||||
|
loading: () => MessagePage(
|
||||||
|
title: Text(l10n.s_slots),
|
||||||
|
graphic: const CircularProgressIndicator(),
|
||||||
|
delayedContent: true,
|
||||||
|
),
|
||||||
|
error: (error, _) =>
|
||||||
|
AppFailurePage(title: Text(l10n.s_slots), cause: error),
|
||||||
|
data: (otpState) {
|
||||||
|
return AppPage(
|
||||||
|
title: Text(l10n.s_slots),
|
||||||
|
keyActionsBuilder: hasFeature(features.actions)
|
||||||
|
? (context) =>
|
||||||
|
otpBuildActions(context, devicePath, otpState, ref)
|
||||||
|
: null,
|
||||||
|
child: Column(children: [
|
||||||
|
ListTitle(l10n.s_slots),
|
||||||
|
...otpState.slots.map((e) => registerOtpActions(devicePath, e,
|
||||||
|
ref: ref,
|
||||||
|
actions: {
|
||||||
|
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) async {
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
builder: (context) => SlotDialog(e.slot),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
builder: (context) => _SlotListItem(e)))
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlotListItem extends ConsumerWidget {
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
const _SlotListItem(this.otpSlot);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final slot = otpSlot.slot;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final isConfigured = otpSlot.isConfigured;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
label: slot.getDisplayName(l10n),
|
||||||
|
child: AppListItem(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
foregroundColor: colorScheme.onSecondary,
|
||||||
|
backgroundColor: colorScheme.secondary,
|
||||||
|
child: Text(slot.numberId.toString())),
|
||||||
|
title: slot.getDisplayName(l10n),
|
||||||
|
subtitle:
|
||||||
|
isConfigured ? l10n.l_otp_slot_configured : l10n.l_otp_slot_empty,
|
||||||
|
trailing: OutlinedButton(
|
||||||
|
onPressed: Actions.handler(context, const OpenIntent()),
|
||||||
|
child: const Icon(Icons.more_horiz),
|
||||||
|
),
|
||||||
|
buildPopupActions: hasFeature(features.slots)
|
||||||
|
? (context) => buildSlotActions(isConfigured, l10n)
|
||||||
|
: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
66
lib/otp/views/overwrite_confirm_dialog.dart
Normal file
66
lib/otp/views/overwrite_confirm_dialog.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
|
||||||
|
class _OverwriteConfirmDialog extends StatelessWidget {
|
||||||
|
final OtpSlot otpSlot;
|
||||||
|
|
||||||
|
const _OverwriteConfirmDialog({
|
||||||
|
required this.otpSlot,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_overwrite_slot),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: Text(l10n.s_overwrite)),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.p_overwrite_slot_desc(otpSlot.slot.getDisplayName(l10n))),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> confirmOverwrite(BuildContext context, OtpSlot otpSlot) async {
|
||||||
|
if (otpSlot.isConfigured) {
|
||||||
|
return await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _OverwriteConfirmDialog(
|
||||||
|
otpSlot: otpSlot,
|
||||||
|
)) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
93
lib/otp/views/slot_dialog.dart
Normal file
93
lib/otp/views/slot_dialog.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../app/views/action_list.dart';
|
||||||
|
import '../../app/views/fs_dialog.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'actions.dart';
|
||||||
|
|
||||||
|
class SlotDialog extends ConsumerWidget {
|
||||||
|
final SlotId slot;
|
||||||
|
const SlotDialog(this.slot, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// TODO: Solve this in a cleaner way
|
||||||
|
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
|
||||||
|
if (node == null) {
|
||||||
|
// The rest of this method assumes there is a device, and will throw an exception if not.
|
||||||
|
// This will never be shown, as the dialog will be immediately closed
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
final otpState = ref.watch(otpStateProvider(node.path)).valueOrNull;
|
||||||
|
final otpSlot =
|
||||||
|
otpState!.slots.firstWhereOrNull((element) => element.slot == slot);
|
||||||
|
|
||||||
|
if (otpSlot == null) {
|
||||||
|
return const FsDialog(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return registerOtpActions(node.path, otpSlot,
|
||||||
|
ref: ref,
|
||||||
|
builder: (context) => FocusScope(
|
||||||
|
autofocus: true,
|
||||||
|
child: FsDialog(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 48, bottom: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
otpSlot.slot.getDisplayName(l10n),
|
||||||
|
style: textTheme.headlineSmall,
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Icon(
|
||||||
|
Icons.touch_app,
|
||||||
|
size: 100.0,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(otpSlot.isConfigured
|
||||||
|
? l10n.l_otp_slot_configured
|
||||||
|
: l10n.l_otp_slot_empty)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionListSection.fromMenuActions(
|
||||||
|
context,
|
||||||
|
l10n.s_setup,
|
||||||
|
actions: buildSlotActions(otpSlot.isConfigured, l10n),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
63
lib/otp/views/swap_slots_dialog.dart
Normal file
63
lib/otp/views/swap_slots_dialog.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Yubico.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
|
||||||
|
class SwapSlotsDialog extends ConsumerWidget {
|
||||||
|
final DevicePath devicePath;
|
||||||
|
const SwapSlotsDialog(this.devicePath, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_swap_slots),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref.read(otpStateProvider(devicePath).notifier).swapSlots();
|
||||||
|
await ref.read(withContextProvider)((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showMessage(context, l10n.l_slots_swapped);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(l10n.s_swap))
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.p_swap_slots_desc),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,12 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -40,6 +41,7 @@ class AuthenticationDialog extends ConsumerStatefulWidget {
|
|||||||
class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||||
bool _defaultKeyUsed = false;
|
bool _defaultKeyUsed = false;
|
||||||
bool _keyIsWrong = false;
|
bool _keyIsWrong = false;
|
||||||
|
bool _keyFormatInvalid = false;
|
||||||
final _keyController = TextEditingController();
|
final _keyController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -56,6 +58,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
ManagementKeyType.tdes)
|
ManagementKeyType.tdes)
|
||||||
.keyLength *
|
.keyLength *
|
||||||
2;
|
2;
|
||||||
|
final keyFormatInvalid = !Format.hex.isValid(_keyController.text);
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(l10n.l_unlock_piv_management),
|
title: Text(l10n.l_unlock_piv_management),
|
||||||
actions: [
|
actions: [
|
||||||
@ -63,6 +66,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: _keyController.text.length == keyLen
|
onPressed: _keyController.text.length == keyLen
|
||||||
? () async {
|
? () async {
|
||||||
|
if (keyFormatInvalid) {
|
||||||
|
setState(() {
|
||||||
|
_keyFormatInvalid = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
try {
|
try {
|
||||||
final status = await ref
|
final status = await ref
|
||||||
@ -99,19 +108,20 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
controller: _keyController,
|
controller: _keyController,
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.allow(
|
|
||||||
RegExp('[a-f0-9]', caseSensitive: false))
|
|
||||||
],
|
|
||||||
readOnly: _defaultKeyUsed,
|
readOnly: _defaultKeyUsed,
|
||||||
maxLength: !_defaultKeyUsed ? keyLen : null,
|
maxLength: !_defaultKeyUsed ? keyLen : null,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_management_key,
|
labelText: l10n.s_management_key,
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
|
||||||
errorText: _keyIsWrong ? l10n.l_wrong_key : null,
|
|
||||||
errorMaxLines: 3,
|
|
||||||
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
||||||
|
errorText: _keyIsWrong
|
||||||
|
? l10n.l_wrong_key
|
||||||
|
: _keyFormatInvalid
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
errorMaxLines: 3,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
suffixIcon: hasMetadata
|
suffixIcon: hasMetadata
|
||||||
? null
|
? null
|
||||||
: IconButton(
|
: IconButton(
|
||||||
@ -121,6 +131,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
tooltip: l10n.s_use_default,
|
tooltip: l10n.s_use_default,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_keyFormatInvalid = false;
|
||||||
_defaultKeyUsed = !_defaultKeyUsed;
|
_defaultKeyUsed = !_defaultKeyUsed;
|
||||||
if (_defaultKeyUsed) {
|
if (_defaultKeyUsed) {
|
||||||
_keyController.text = defaultManagementKey;
|
_keyController.text = defaultManagementKey;
|
||||||
@ -135,6 +146,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyIsWrong = false;
|
_keyIsWrong = false;
|
||||||
|
_keyFormatInvalid = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -22,6 +22,7 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/choice_filter_chip.dart';
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
@ -161,12 +162,13 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
|||||||
AppTextField(
|
AppTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
key: keys.subjectField,
|
key: keys.subjectField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_subject,
|
labelText: l10n.s_subject,
|
||||||
errorText: _subject.isNotEmpty && _invalidSubject
|
errorText: _subject.isNotEmpty && _invalidSubject
|
||||||
? l10n.l_rfc4514_invalid
|
? l10n.l_rfc4514_invalid
|
||||||
: null),
|
: null,
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
enabled: !_generating,
|
enabled: !_generating,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -51,6 +52,7 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
|||||||
String _password = '';
|
String _password = '';
|
||||||
bool _passwordIsWrong = false;
|
bool _passwordIsWrong = false;
|
||||||
bool _importing = false;
|
bool _importing = false;
|
||||||
|
bool _isObscure = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -125,15 +127,27 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
|||||||
Text(l10n.p_password_protected_file),
|
Text(l10n.p_password_protected_file),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_password,
|
labelText: l10n.s_password,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
|
||||||
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
|
errorMaxLines: 3,
|
||||||
errorMaxLines: 3),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscure ? Icons.visibility : Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscure = !_isObscure;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscure
|
||||||
|
? l10n.s_show_password
|
||||||
|
: l10n.s_hide_password),
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -17,13 +17,14 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/app_text_form_field.dart';
|
import '../../widgets/app_text_form_field.dart';
|
||||||
import '../../widgets/choice_filter_chip.dart';
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
@ -49,10 +50,13 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
late bool _usesStoredKey;
|
late bool _usesStoredKey;
|
||||||
late bool _storeKey;
|
late bool _storeKey;
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
|
bool _currentInvalidFormat = false;
|
||||||
|
bool _newInvalidFormat = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
ManagementKeyType _keyType = ManagementKeyType.tdes;
|
ManagementKeyType _keyType = ManagementKeyType.tdes;
|
||||||
final _currentController = TextEditingController();
|
final _currentController = TextEditingController();
|
||||||
final _keyController = TextEditingController();
|
final _keyController = TextEditingController();
|
||||||
|
bool _isObscure = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -76,6 +80,16 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
|
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
|
||||||
|
final newInvalidFormat = Format.hex.isValid(_keyController.text);
|
||||||
|
if (!currentInvalidFormat || !newInvalidFormat) {
|
||||||
|
setState(() {
|
||||||
|
_currentInvalidFormat = !currentInvalidFormat;
|
||||||
|
_newInvalidFormat = !newInvalidFormat;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
||||||
if (_usesStoredKey) {
|
if (_usesStoredKey) {
|
||||||
final status = (await notifier.verifyPin(_currentController.text)).when(
|
final status = (await notifier.verifyPin(_currentController.text)).when(
|
||||||
@ -155,24 +169,37 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
if (protected)
|
if (protected)
|
||||||
AppTextField(
|
AppTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.pinPukField,
|
key: keys.pinPukField,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
controller: _currentController,
|
controller: _currentController,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
errorText: _currentIsWrong
|
||||||
errorText: _currentIsWrong
|
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||||
? l10n
|
: _currentInvalidFormat
|
||||||
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
: null,
|
Format.hex.allowedCharacters)
|
||||||
errorMaxLines: 3),
|
: null,
|
||||||
|
errorMaxLines: 3,
|
||||||
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscure ? Icons.visibility : Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscure = !_isObscure;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin),
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = false;
|
_currentIsWrong = false;
|
||||||
|
_currentInvalidFormat = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -184,13 +211,18 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
controller: _currentController,
|
controller: _currentController,
|
||||||
readOnly: _defaultKeyUsed,
|
readOnly: _defaultKeyUsed,
|
||||||
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_management_key,
|
labelText: l10n.s_current_management_key,
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
|
||||||
errorText: _currentIsWrong ? l10n.l_wrong_key : null,
|
|
||||||
errorMaxLines: 3,
|
|
||||||
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
||||||
|
errorText: _currentIsWrong
|
||||||
|
? l10n.l_wrong_key
|
||||||
|
: _currentInvalidFormat
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
|
errorMaxLines: 3,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
suffixIcon: _hasMetadata
|
suffixIcon: _hasMetadata
|
||||||
? null
|
? null
|
||||||
: IconButton(
|
: IconButton(
|
||||||
@ -210,10 +242,6 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
inputFormatters: <TextInputFormatter>[
|
|
||||||
FilteringTextInputFormatter.allow(
|
|
||||||
RegExp('[a-f0-9]', caseSensitive: false))
|
|
||||||
],
|
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -227,15 +255,15 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
autofillHints: const [AutofillHints.newPassword],
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
maxLength: hexLength,
|
maxLength: hexLength,
|
||||||
controller: _keyController,
|
controller: _keyController,
|
||||||
inputFormatters: <TextInputFormatter>[
|
decoration: AppInputDecoration(
|
||||||
FilteringTextInputFormatter.allow(
|
|
||||||
RegExp('[a-f0-9]', caseSensitive: false))
|
|
||||||
],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_new_management_key,
|
labelText: l10n.s_new_management_key,
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
errorText: _newInvalidFormat
|
||||||
|
? l10n.l_invalid_format_allowed_chars(
|
||||||
|
Format.hex.allowedCharacters)
|
||||||
|
: null,
|
||||||
enabled: currentLenOk,
|
enabled: currentLenOk,
|
||||||
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
key: keys.managementKeyRefresh,
|
key: keys.managementKeyRefresh,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
@ -251,6 +279,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
.padLeft(2, '0')).join();
|
.padLeft(2, '0')).join();
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyController.text = key;
|
_keyController.text = key;
|
||||||
|
_newInvalidFormat = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -44,6 +45,9 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
String _confirmPin = '';
|
String _confirmPin = '';
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
|
bool _isObscureCurrent = true;
|
||||||
|
bool _isObscureNew = true;
|
||||||
|
bool _isObscureConfirm = true;
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
||||||
@ -104,24 +108,38 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
: l10n.p_enter_current_puk_or_reset),
|
: l10n.p_enter_current_puk_or_reset),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscureCurrent,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.pinPukField,
|
key: keys.pinPukField,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: widget.target == ManageTarget.pin
|
labelText: widget.target == ManageTarget.pin
|
||||||
? l10n.s_current_pin
|
? l10n.s_current_pin
|
||||||
: l10n.s_current_puk,
|
: l10n.s_current_puk,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
errorText: _currentIsWrong
|
||||||
errorText: _currentIsWrong
|
? (widget.target == ManageTarget.pin
|
||||||
? (widget.target == ManageTarget.pin
|
? l10n
|
||||||
? l10n.l_wrong_pin_attempts_remaining(
|
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||||
_attemptsRemaining)
|
: l10n
|
||||||
: l10n.l_wrong_puk_attempts_remaining(
|
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
|
||||||
_attemptsRemaining))
|
: null,
|
||||||
: null,
|
errorMaxLines: 3,
|
||||||
errorMaxLines: 3),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureCurrent
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureCurrent = !_isObscureCurrent;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: widget.target == ManageTarget.pin
|
||||||
|
? (_isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||||
|
: (_isObscureCurrent ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||||
|
),
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -134,15 +152,27 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)),
|
widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
key: keys.newPinPukField,
|
key: keys.newPinPukField,
|
||||||
obscureText: true,
|
obscureText: _isObscureNew,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
autofillHints: const [AutofillHints.newPassword],
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: widget.target == ManageTarget.puk
|
labelText: widget.target == ManageTarget.puk
|
||||||
? l10n.s_new_puk
|
? l10n.s_new_puk
|
||||||
: l10n.s_new_pin,
|
: l10n.s_new_pin,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscureNew ? Icons.visibility : Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureNew = !_isObscureNew;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: widget.target == ManageTarget.pin
|
||||||
|
? (_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
|
// Old YubiKeys allowed a 4 digit PIN
|
||||||
enabled: _currentPin.length >= 4,
|
enabled: _currentPin.length >= 4,
|
||||||
),
|
),
|
||||||
@ -160,15 +190,28 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
),
|
),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
key: keys.confirmPinPukField,
|
key: keys.confirmPinPukField,
|
||||||
obscureText: true,
|
obscureText: _isObscureConfirm,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
autofillHints: const [AutofillHints.newPassword],
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: widget.target == ManageTarget.puk
|
labelText: widget.target == ManageTarget.puk
|
||||||
? l10n.s_confirm_puk
|
? l10n.s_confirm_puk
|
||||||
: l10n.s_confirm_pin,
|
: l10n.s_confirm_pin,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(_isObscureConfirm
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscureConfirm = !_isObscureConfirm;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: widget.target == ManageTarget.pin
|
||||||
|
? (_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: _currentPin.length >= 4 && _newPin.length >= 6,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
|
@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
|
import '../../widgets/app_input_decoration.dart';
|
||||||
import '../../widgets/app_text_field.dart';
|
import '../../widgets/app_text_field.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -93,19 +94,18 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
controller: _pinController,
|
controller: _pinController,
|
||||||
decoration: InputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
|
||||||
errorText: _pinIsWrong
|
errorText: _pinIsWrong
|
||||||
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||||
: null,
|
: null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||||
color: IconTheme.of(context).color,
|
color: !_pinIsWrong ? IconTheme.of(context).color : null),
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
|
@ -46,18 +46,18 @@ class PivScreen extends ConsumerWidget {
|
|||||||
final hasFeature = ref.watch(featureProvider);
|
final hasFeature = ref.watch(featureProvider);
|
||||||
return ref.watch(pivStateProvider(devicePath)).when(
|
return ref.watch(pivStateProvider(devicePath)).when(
|
||||||
loading: () => MessagePage(
|
loading: () => MessagePage(
|
||||||
title: Text(l10n.s_piv),
|
title: Text(l10n.s_certificates),
|
||||||
graphic: const CircularProgressIndicator(),
|
graphic: const CircularProgressIndicator(),
|
||||||
delayedContent: true,
|
delayedContent: true,
|
||||||
),
|
),
|
||||||
error: (error, _) => AppFailurePage(
|
error: (error, _) => AppFailurePage(
|
||||||
title: Text(l10n.s_piv),
|
title: Text(l10n.s_certificates),
|
||||||
cause: error,
|
cause: error,
|
||||||
),
|
),
|
||||||
data: (pivState) {
|
data: (pivState) {
|
||||||
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: Text(l10n.s_piv),
|
title: Text(l10n.s_certificates),
|
||||||
keyActionsBuilder: hasFeature(features.actions)
|
keyActionsBuilder: hasFeature(features.actions)
|
||||||
? (context) =>
|
? (context) =>
|
||||||
pivBuildActions(context, devicePath, pivState, ref)
|
pivBuildActions(context, devicePath, pivState, ref)
|
||||||
|
101
lib/widgets/app_input_decoration.dart
Normal file
101
lib/widgets/app_input_decoration.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppInputDecoration extends InputDecoration {
|
||||||
|
final List<Widget>? suffixIcons;
|
||||||
|
|
||||||
|
const AppInputDecoration({
|
||||||
|
// allow multiple suffixIcons
|
||||||
|
this.suffixIcons,
|
||||||
|
// forward other TextField parameters
|
||||||
|
super.icon,
|
||||||
|
super.iconColor,
|
||||||
|
super.label,
|
||||||
|
super.labelText,
|
||||||
|
super.labelStyle,
|
||||||
|
super.floatingLabelStyle,
|
||||||
|
super.helperText,
|
||||||
|
super.helperStyle,
|
||||||
|
super.helperMaxLines,
|
||||||
|
super.hintText,
|
||||||
|
super.hintStyle,
|
||||||
|
super.hintTextDirection,
|
||||||
|
super.hintMaxLines,
|
||||||
|
super.hintFadeDuration,
|
||||||
|
super.error,
|
||||||
|
super.errorText,
|
||||||
|
super.errorStyle,
|
||||||
|
super.errorMaxLines,
|
||||||
|
super.floatingLabelBehavior,
|
||||||
|
super.floatingLabelAlignment,
|
||||||
|
super.isCollapsed,
|
||||||
|
super.isDense,
|
||||||
|
super.contentPadding,
|
||||||
|
super.prefixIcon,
|
||||||
|
super.prefixIconConstraints,
|
||||||
|
super.prefix,
|
||||||
|
super.prefixText,
|
||||||
|
super.prefixStyle,
|
||||||
|
super.prefixIconColor,
|
||||||
|
super.suffixIcon,
|
||||||
|
super.suffix,
|
||||||
|
super.suffixText,
|
||||||
|
super.suffixStyle,
|
||||||
|
super.suffixIconColor,
|
||||||
|
super.suffixIconConstraints,
|
||||||
|
super.counter,
|
||||||
|
super.counterText,
|
||||||
|
super.counterStyle,
|
||||||
|
super.filled,
|
||||||
|
super.fillColor,
|
||||||
|
super.focusColor,
|
||||||
|
super.hoverColor,
|
||||||
|
super.errorBorder,
|
||||||
|
super.focusedBorder,
|
||||||
|
super.focusedErrorBorder,
|
||||||
|
super.disabledBorder,
|
||||||
|
super.enabledBorder,
|
||||||
|
super.border,
|
||||||
|
super.enabled = true,
|
||||||
|
super.semanticCounterText,
|
||||||
|
super.alignLabelWithHint,
|
||||||
|
super.constraints,
|
||||||
|
}) : assert(!(suffixIcon != null && suffixIcons != null),
|
||||||
|
'Declaring both suffixIcon and suffixIcons is not supported.');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get suffixIcon {
|
||||||
|
final icons = [
|
||||||
|
if (super.suffixIcon != null) super.suffixIcon!,
|
||||||
|
if (suffixIcons != null) ...suffixIcons!,
|
||||||
|
if (errorText != null) const Icon(Icons.error_outlined),
|
||||||
|
];
|
||||||
|
|
||||||
|
return switch (icons.length) {
|
||||||
|
0 => null,
|
||||||
|
1 => icons.single,
|
||||||
|
_ => Builder(
|
||||||
|
builder: (context) {
|
||||||
|
// Apply the constraints to *each* icon.
|
||||||
|
final constraints = suffixIconConstraints ??
|
||||||
|
Theme.of(context).visualDensity.effectiveConstraints(
|
||||||
|
const BoxConstraints(
|
||||||
|
minWidth: kMinInteractiveDimension,
|
||||||
|
minHeight: kMinInteractiveDimension,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
for (Widget icon in icons)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: constraints,
|
||||||
|
child: icon,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'app_input_decoration.dart';
|
||||||
|
|
||||||
/// TextField without autocorrect and suggestions
|
/// TextField without autocorrect and suggestions
|
||||||
class AppTextField extends TextField {
|
class AppTextField extends TextField {
|
||||||
const AppTextField({
|
const AppTextField({
|
||||||
@ -28,7 +30,7 @@ class AppTextField extends TextField {
|
|||||||
super.controller,
|
super.controller,
|
||||||
super.focusNode,
|
super.focusNode,
|
||||||
super.undoController,
|
super.undoController,
|
||||||
super.decoration,
|
AppInputDecoration? decoration,
|
||||||
super.textInputAction,
|
super.textInputAction,
|
||||||
super.textCapitalization,
|
super.textCapitalization,
|
||||||
super.style,
|
super.style,
|
||||||
@ -83,5 +85,5 @@ class AppTextField extends TextField {
|
|||||||
super.canRequestFocus,
|
super.canRequestFocus,
|
||||||
super.spellCheckConfiguration,
|
super.spellCheckConfiguration,
|
||||||
super.magnifierConfiguration,
|
super.magnifierConfiguration,
|
||||||
});
|
}) : super(decoration: decoration);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'app_input_decoration.dart';
|
||||||
|
|
||||||
/// TextFormField without autocorrect and suggestions
|
/// TextFormField without autocorrect and suggestions
|
||||||
class AppTextFormField extends TextFormField {
|
class AppTextFormField extends TextFormField {
|
||||||
AppTextFormField({
|
AppTextFormField({
|
||||||
@ -28,7 +30,7 @@ class AppTextFormField extends TextFormField {
|
|||||||
super.controller,
|
super.controller,
|
||||||
super.initialValue,
|
super.initialValue,
|
||||||
super.focusNode,
|
super.focusNode,
|
||||||
super.decoration,
|
AppInputDecoration? decoration,
|
||||||
super.textCapitalization,
|
super.textCapitalization,
|
||||||
super.textInputAction,
|
super.textInputAction,
|
||||||
super.style,
|
super.style,
|
||||||
@ -87,5 +89,5 @@ class AppTextFormField extends TextFormField {
|
|||||||
super.clipBehavior,
|
super.clipBehavior,
|
||||||
super.scribbleEnabled,
|
super.scribbleEnabled,
|
||||||
super.canRequestFocus,
|
super.canRequestFocus,
|
||||||
});
|
}) : super(decoration: decoration);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import 'package:flutter/material.dart';
|
|||||||
class ChoiceFilterChip<T> extends StatefulWidget {
|
class ChoiceFilterChip<T> extends StatefulWidget {
|
||||||
final T value;
|
final T value;
|
||||||
final List<T> items;
|
final List<T> items;
|
||||||
|
final String? tooltip;
|
||||||
final Widget Function(T value) itemBuilder;
|
final Widget Function(T value) itemBuilder;
|
||||||
final Widget Function(T value)? labelBuilder;
|
final Widget Function(T value)? labelBuilder;
|
||||||
final void Function(T value)? onChanged;
|
final void Function(T value)? onChanged;
|
||||||
@ -32,6 +33,7 @@ class ChoiceFilterChip<T> extends StatefulWidget {
|
|||||||
required this.items,
|
required this.items,
|
||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
this.tooltip,
|
||||||
this.avatar,
|
this.avatar,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.labelBuilder,
|
this.labelBuilder,
|
||||||
@ -57,7 +59,6 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
|||||||
),
|
),
|
||||||
Offset.zero & overlay.size,
|
Offset.zero & overlay.size,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await showMenu(
|
return await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
position: position,
|
position: position,
|
||||||
@ -79,6 +80,7 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
|
tooltip: widget.tooltip,
|
||||||
avatar: widget.avatar,
|
avatar: widget.avatar,
|
||||||
labelPadding: const EdgeInsets.only(left: 4),
|
labelPadding: const EdgeInsets.only(left: 4),
|
||||||
label: Row(
|
label: Row(
|
||||||
|
Loading…
Reference in New Issue
Block a user