Merge branch 'main' into dain/restricted-nfc

This commit is contained in:
Adam Velebil 2024-08-26 16:09:42 +02:00
commit 73f8ae5348
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
25 changed files with 697 additions and 501 deletions

View File

@ -1,2 +1,2 @@
FLUTTER=3.24.0
FLUTTER=3.24.1
PYVER=3.12.5

View File

@ -42,6 +42,7 @@ import com.yubico.authenticator.oath.keystore.ClearingMemProvider
import com.yubico.authenticator.oath.keystore.KeyProvider
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
import com.yubico.authenticator.oath.keystore.SharedPrefProvider
import com.yubico.authenticator.yubikit.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
@ -105,6 +106,7 @@ class OathManager(
private var pendingAction: OathAction? = null
private var refreshJob: Job? = null
private var addToAny = false
private val updateDeviceInfo = AtomicBoolean(false)
override fun onPause() {
// cancel any pending actions, except for addToAny
@ -284,6 +286,10 @@ class OathManager(
logger.debug(
"Successfully read Oath session info (and credentials if unlocked) from connected key"
)
if (updateDeviceInfo.getAndSet(false)) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
} catch (e: Exception) {
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Failed to connect to CCID: ", e)
@ -362,7 +368,7 @@ class OathManager(
}
private suspend fun reset(): String =
useOathSession(OathActionDescription.Reset) {
useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) {
// note, it is ok to reset locked session
it.reset()
keyManager.removeKey(it.deviceId)
@ -396,7 +402,11 @@ class OathManager(
currentPassword: String?,
newPassword: String,
): String =
useOathSession(OathActionDescription.SetPassword, unlock = false) { session ->
useOathSession(
OathActionDescription.SetPassword,
unlock = false,
updateDeviceInfo = true
) { session ->
if (session.isAccessKeySet) {
if (currentPassword == null) {
throw Exception("Must provide current password to be able to change it")
@ -648,22 +658,30 @@ class OathManager(
private suspend fun <T> useOathSession(
oathActionDescription: OathActionDescription,
unlock: Boolean = true,
updateDeviceInfo: Boolean = false,
action: (YubiKitOathSession) -> T
): T {
// callers can decide whether the session should be unlocked first
unlockOnConnect.set(unlock)
// callers can request whether device info should be updated after session operation
this@OathManager.updateDeviceInfo.set(updateDeviceInfo)
return deviceManager.withKey(
onUsb = { useOathSessionUsb(it, action) },
onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) },
onNfc = { useOathSessionNfc(oathActionDescription, action) }
)
}
private suspend fun <T> useOathSessionUsb(
device: UsbYubiKeyDevice,
updateDeviceInfo: Boolean = false,
block: (YubiKitOathSession) -> T
): T = device.withConnection<SmartCardConnection, T> {
block(getOathSession(it))
}.also {
if (updateDeviceInfo) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
}
private suspend fun <T> useOathSessionNfc(

View File

@ -1,2 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources />
<resources>
<string name="p_ndef_set_otp">OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.</string>
<string name="p_ndef_set_password">Passwort wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.</string>
<string name="p_ndef_parse_failure">Beim Parsen des OTP-Codes von Ihrem YubiKey ist ein Fehler aufgetreten.</string>
<string name="p_ndef_set_clip_failure">Konnte während dem Versuch den OTP-Code von Ihrem YubiKey zu kopieren nicht auf die Zwischenablage zugreifen.</string>
</resources>

View File

@ -51,6 +51,7 @@ from ykman.pcsc import list_devices, YK_READER_NAME
from smartcard.Exceptions import SmartcardException, NoCardException
from smartcard.pcsc.PCSCExceptions import EstablishContextException
from smartcard.CardMonitoring import CardObserver, CardMonitor
from fido2.ctap import CtapError
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from hashlib import sha256
from dataclasses import asdict
@ -355,6 +356,11 @@ class UsbDeviceNode(AbstractDeviceNode):
except (ValueError, OSError) as e:
logger.warning("Error opening connection", exc_info=True)
raise ConnectionException(self._device.fingerprint, "fido", e)
except Exception as e: # TODO: Replace with ConnectionError once added
if "Wrong" in str(e):
logger.warning("Error opening connection", exc_info=True)
raise ConnectionException(self._device.fingerprint, "fido", e)
raise
class _ReaderObserver(CardObserver):
@ -467,6 +473,14 @@ class ConnectionNode(RpcNode):
if e.sw == SW.INVALID_INSTRUCTION:
raise ChildResetException(f"SW: {e.sw}")
raise e
except CtapError as e:
if e.code == CtapError.ERR.CHANNEL_BUSY:
raise ChildResetException(str(e))
raise
except Exception as e: # TODO: Replace with ConnectionError once added
if "Wrong" in str(e):
raise ChildResetException(str(e))
raise
@property
def capabilities(self):

View File

@ -581,6 +581,7 @@ class _AppPageState extends ConsumerState<AppPage> {
Expanded(child: body),
if (hasManage &&
!hasDetailsOrKeyActions &&
showDetailView &&
widget.capabilities != null &&
widget.capabilities?.first != Capability.u2f)
// Add a placeholder for the Manage/Details column. Exceptions are:
@ -684,8 +685,7 @@ class _AppPageState extends ConsumerState<AppPage> {
),
actions: [
if (widget.actionButtonBuilder == null &&
(widget.keyActionsBuilder != null &&
(!hasManage || !showDetailView)))
(widget.keyActionsBuilder != null && !hasManage))
Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(

View File

@ -72,6 +72,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
StreamSubscription<InteractionEvent>? _subscription;
InteractionEvent? _interaction;
int _currentStep = -1;
bool _resetting = false;
late final int _totalSteps;
@override
@ -125,6 +126,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
return ResponsiveDialog(
title: Text(l10n.s_factory_reset),
key: factoryResetCancel,
allowCancel: !_resetting || _application == Capability.fido2,
onCancel: switch (_application) {
Capability.fido2 => _currentStep < _totalSteps
? () {
@ -144,83 +146,97 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
actions: [
if (_currentStep < _totalSteps)
TextButton(
onPressed: switch (_application) {
Capability.fido2 => _subscription == null
? () async {
_subscription = ref
.read(
fidoStateProvider(widget.data.node.path).notifier)
.reset()
.listen((event) {
setState(() {
_currentStep++;
_interaction = event;
});
}, onDone: () {
setState(() {
_currentStep++;
});
_subscription = null;
}, onError: (e) {
_log.error('Error performing FIDO reset', e);
onPressed: !_resetting
? switch (_application) {
Capability.fido2 => () async {
_subscription = ref
.read(fidoStateProvider(widget.data.node.path)
.notifier)
.reset()
.listen((event) {
setState(() {
_resetting = true;
_currentStep++;
_interaction = event;
});
}, onDone: () {
setState(() {
_currentStep++;
});
_subscription = null;
}, onError: (e) {
_log.error('Error performing FIDO reset', e);
if (!context.mounted) return;
Navigator.of(context).pop();
final String errorMessage;
// TODO: Make this cleaner than importing desktop specific RpcError.
if (e is RpcError) {
if (e.status == 'connection-error') {
errorMessage = l10n.l_failed_connecting_to_fido;
} else if (e.status == 'key-mismatch') {
errorMessage = l10n.l_wrong_inserted_yk_error;
} else if (e.status == 'user-action-timeout') {
errorMessage = l10n.l_user_action_timeout_error;
if (!context.mounted) return;
Navigator.of(context).pop();
final String errorMessage;
// TODO: Make this cleaner than importing desktop specific RpcError.
if (e is RpcError) {
if (e.status == 'connection-error') {
errorMessage = l10n.l_failed_connecting_to_fido;
} else if (e.status == 'key-mismatch') {
errorMessage = l10n.l_wrong_inserted_yk_error;
} else if (e.status == 'user-action-timeout') {
errorMessage = l10n.l_user_action_timeout_error;
} else {
errorMessage = e.message;
}
} else {
errorMessage = e.message;
errorMessage = e.toString();
}
} else {
errorMessage = e.toString();
}
showMessage(
context,
l10n.l_reset_failed(errorMessage),
duration: const Duration(seconds: 4),
);
});
}
: null,
Capability.oath => () async {
await ref
.read(oathStateProvider(widget.data.node.path).notifier)
.reset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.l_oath_application_reset);
});
},
Capability.piv => () async {
await ref
.read(pivStateProvider(widget.data.node.path).notifier)
.reset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.l_piv_app_reset);
});
},
null => globalReset
? () async {
await ref
.read(managementStateProvider(widget.data.node.path)
.notifier)
.deviceReset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.s_factory_reset);
});
}
: null,
_ => throw UnsupportedError('Application cannot be reset'),
},
showMessage(
context,
l10n.l_reset_failed(errorMessage),
duration: const Duration(seconds: 4),
);
});
},
Capability.oath => () async {
setState(() {
_resetting = true;
});
await ref
.read(oathStateProvider(widget.data.node.path)
.notifier)
.reset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.l_oath_application_reset);
});
},
Capability.piv => () async {
setState(() {
_resetting = true;
});
await ref
.read(pivStateProvider(widget.data.node.path)
.notifier)
.reset();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.l_piv_app_reset);
});
},
null => globalReset
? () async {
setState(() {
_resetting = true;
});
await ref
.read(managementStateProvider(
widget.data.node.path)
.notifier)
.deviceReset();
await ref.read(withContextProvider)(
(context) async {
Navigator.of(context).pop();
showMessage(context, l10n.s_factory_reset);
});
}
: null,
_ => throw UnsupportedError('Application cannot be reset'),
}
: null,
key: factoryResetReset,
child: Text(l10n.s_reset),
)
@ -306,10 +322,12 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
},
),
],
if (_application == Capability.fido2 && _currentStep >= 0) ...[
Text('${l10n.s_status}: ${_getMessage()}'),
LinearProgressIndicator(value: progress)
],
if (_resetting)
if (_application == Capability.fido2 && _currentStep >= 0) ...[
Text('${l10n.s_status}: ${_getMessage()}'),
LinearProgressIndicator(value: progress),
] else
const LinearProgressIndicator()
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -36,7 +36,14 @@ class EnableEnterpriseAttestationDialog extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_enable_ep_attestation_desc),
Text(
l10n.p_enable_ep_attestation_desc,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.w700),
),
Text(l10n.p_enable_ep_attestation_disable_with_factory_reset),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -52,12 +52,11 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
final authBlocked = state.pinBlocked;
final enterpriseAttestation = state.enterpriseAttestation;
final showEnterpriseAttestation = enterpriseAttestation != null &&
final showEnterpriseAttestation =
enterpriseAttestation != null && fingerprints == null;
final canEnableEnterpriseAttestation = enterpriseAttestation == false &&
!(state.alwaysUv && !state.hasPin) &&
!(!state.unlocked && state.hasPin) &&
fingerprints == null;
final canEnableEnterpriseAttestation =
enterpriseAttestation == false && showEnterpriseAttestation;
!(!state.unlocked && state.hasPin);
return Column(
children: [
@ -130,8 +129,11 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
feature: features.enableEnterpriseAttestation,
icon: const Icon(Symbols.local_police),
title: l10n.s_ep_attestation,
subtitle:
enterpriseAttestation ? l10n.s_enabled : l10n.s_disabled,
subtitle: enterpriseAttestation
? l10n.s_enabled
: (state.alwaysUv && !state.hasPin)
? l10n.l_set_pin_first
: l10n.s_disabled,
onTap: canEnableEnterpriseAttestation
? (context) {
Navigator.of(context).popUntil((route) => route.isFirst);

View File

@ -379,7 +379,6 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
child: LayoutBuilder(builder: (context, constraints) {
final textTheme = Theme.of(context).textTheme;
final width = constraints.maxWidth;
final showLayoutOptions = width > 600;
return Consumer(
builder: (context, ref, child) {
final layout = ref.watch(passkeysLayoutProvider);
@ -426,9 +425,40 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
setState(() {});
},
),
if (searchController.text.isEmpty && showLayoutOptions)
...FlexLayout.values.map(
(e) => MouseRegion(
if (searchController.text.isEmpty) ...[
if (width >= 450)
...FlexLayout.values.map(
(e) => MouseRegion(
onEnter: (event) {
if (!searchFocus.hasFocus) {
setState(() {
_canRequestFocus = false;
});
}
},
onExit: (event) {
setState(() {
_canRequestFocus = true;
});
},
child: IconButton(
tooltip: e.getDisplayName(l10n),
onPressed: () {
ref
.read(passkeysLayoutProvider.notifier)
.setLayout(e);
},
icon: Icon(
e.icon,
color: e == layout
? Theme.of(context).colorScheme.primary
: null,
),
),
),
),
if (width < 450)
MouseRegion(
onEnter: (event) {
if (!searchFocus.hasFocus) {
setState(() {
@ -441,22 +471,47 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
_canRequestFocus = true;
});
},
child: IconButton(
tooltip: e.getDisplayName(l10n),
onPressed: () {
ref
.read(passkeysLayoutProvider.notifier)
.setLayout(e);
},
child: PopupMenuButton(
constraints: const BoxConstraints.tightFor(),
tooltip: l10n.s_select_layout,
popUpAnimationStyle:
AnimationStyle(duration: Duration.zero),
icon: Icon(
e.icon,
color: e == layout
? Theme.of(context).colorScheme.primary
: null,
layout.icon,
color: Theme.of(context).colorScheme.primary,
),
itemBuilder: (context) => [
...FlexLayout.values.map(
(e) => PopupMenuItem(
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Tooltip(
message: e.getDisplayName(l10n),
child: Icon(
e.icon,
color: e == layout
? Theme.of(context)
.colorScheme
.primary
: null,
),
),
],
),
onTap: () {
ref
.read(
passkeysLayoutProvider.notifier)
.setLayout(e);
},
),
)
],
),
),
),
)
]
],
),
onChanged: (value) {

File diff suppressed because it is too large Load Diff

View File

@ -295,8 +295,8 @@
"l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey",
"l_pin_blocked_reset": "PIN is blocked; factory reset the FIDO application",
"l_pin_blocked": "PIN is blocked",
"l_set_pin_first": "A PIN is required first",
"l_unlock_pin_first": "Unlock with PIN first",
"l_set_pin_first": "A PIN is required",
"l_unlock_pin_first": "Unlock with PIN",
"l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted",
"l_pin_change_required_desc": "A new PIN must be set before you can use this application",
"p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to unblock it with the PUK or reset the YubiKey.",
@ -322,6 +322,7 @@
"s_ep_attestation_enabled": "Enterprise Attestation enabled",
"s_enable_ep_attestation": "Enable Enterprise Attestation",
"p_enable_ep_attestation_desc": "This will enable Enterprise Attestation, allowing authorized domains to uniquely identify your YubiKey.",
"p_enable_ep_attestation_disable_with_factory_reset": "Once enabled, Enterprise Attestation can only be disabled by performing a FIDO factory reset.",
"s_pin_required": "PIN required",
"p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.",
"l_piv_pin_blocked": "Blocked, use PUK to reset",
@ -374,8 +375,8 @@
"s_password_forgotten": "Password forgotten",
"l_keystore_unavailable": "OS Keystore unavailable",
"l_remember_pw_failed": "Failed to remember password",
"l_unlock_first": "Unlock with password first",
"l_set_password_first": "Set a password first",
"l_unlock_first": "Unlock with password",
"l_set_password_first": "Set a password",
"l_enter_oath_pw": "Enter the OATH password for your YubiKey",
"p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
"p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",
@ -444,7 +445,7 @@
"l_delete_account_desc": "Remove the account from your YubiKey",
"s_account_deleted": "Account deleted",
"p_warning_delete_account": "Warning! This action will delete the account from your YubiKey.",
"p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.",
"p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to disable this credential from the website to avoid being locked out of your account.",
"s_account_name": "Account name",
"s_search_accounts": "Search accounts",
"l_accounts_used": "{used} of {capacity} accounts used",
@ -503,6 +504,7 @@
}
},
"@_fingerprints": {},
"s_biometrics": "Biometrics",
"l_fingerprint": "Fingerprint: {label}",
"@l_fingerprint": {
"placeholders": {
@ -663,6 +665,7 @@
"s_allow_fingerprint": "Allow fingerprint",
"p_cert_options_desc": "Key algorithm to use, output format, and expiration date (certificate only).",
"p_cert_options_bio_desc": "Key algorithm to use, output format, expiration date (certificate only), and if biometrics can be used instead of PIN.",
"p_key_options_bio_desc": "Allow biometrics to be used instead of PIN.",
"s_overwrite_slot": "Overwrite slot",
"p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.",
"@p_overwrite_slot_desc": {
@ -818,9 +821,9 @@
"p_factory_reset_an_app": "Factory reset an application on your YubiKey.",
"p_factory_reset_desc": "Data is stored in multiple applications on the YubiKey, some of which can be factory reset independently of each other.\n\nSelect an application above to reset.",
"p_warning_factory_reset": "Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.",
"p_warning_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts, including passkeys, from your YubiKey.",
"p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_piv_reset": "Warning! All data stored for PIV will be irrevocably deleted from your YubiKey.",
"p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory default values.",
"p_warning_global_reset": "Warning! This will irrevocably delete all saved data, including credentials, from your YubiKey.",

View File

@ -322,6 +322,7 @@
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": null,
"p_enable_ep_attestation_disable_with_factory_reset": null,
"s_pin_required": "PIN requis",
"p_pin_required_desc": "L'action que vous allez effectuer nécessite la saisie du PIN PIV.",
"l_piv_pin_blocked": "Bloqué, utilisez PUK pour réinitialiser",
@ -503,6 +504,7 @@
}
},
"@_fingerprints": {},
"s_biometrics": null,
"l_fingerprint": "Empreinte digitale\u00a0: {label}",
"@l_fingerprint": {
"placeholders": {
@ -663,6 +665,7 @@
"s_allow_fingerprint": null,
"p_cert_options_desc": "Algorithme clé à utiliser, format de sortie et date d'expiration (certificat uniquement).",
"p_cert_options_bio_desc": null,
"p_key_options_bio_desc": null,
"s_overwrite_slot": "Écraser slot",
"p_overwrite_slot_desc": "Cela écrasera définitivement le contenu du slot {slot}.",
"@p_overwrite_slot_desc": {

View File

@ -322,6 +322,7 @@
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": null,
"p_enable_ep_attestation_disable_with_factory_reset": null,
"s_pin_required": "PINが必要",
"p_pin_required_desc": "実行しようとしているアクションでは、PIV PINを入力する必要があります。",
"l_piv_pin_blocked": "ブロックされています。リセットするにはPUKを使用してください",
@ -503,6 +504,7 @@
}
},
"@_fingerprints": {},
"s_biometrics": null,
"l_fingerprint": "指紋:{label}",
"@l_fingerprint": {
"placeholders": {
@ -663,6 +665,7 @@
"s_allow_fingerprint": null,
"p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限(証明書のみ)。",
"p_cert_options_bio_desc": null,
"p_key_options_bio_desc": null,
"s_overwrite_slot": "スロットを上書き",
"p_overwrite_slot_desc": "これにより、スロット{slot}内の既存コンテンツが完全に上書きされます。",
"@p_overwrite_slot_desc": {

View File

@ -322,6 +322,7 @@
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": null,
"p_enable_ep_attestation_disable_with_factory_reset": null,
"s_pin_required": "Wymagany PIN",
"p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.",
"l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować",
@ -503,6 +504,7 @@
}
},
"@_fingerprints": {},
"s_biometrics": null,
"l_fingerprint": "Odcisk palca: {label}",
"@l_fingerprint": {
"placeholders": {
@ -663,6 +665,7 @@
"s_allow_fingerprint": null,
"p_cert_options_desc": "Algorytm klucza do użycia, format wyjściowy i data wygaśnięcia (tylko certyfikat).",
"p_cert_options_bio_desc": null,
"p_key_options_bio_desc": null,
"s_overwrite_slot": "Nadpisz slot",
"p_overwrite_slot_desc": "Spowoduje to trwałe nadpisanie istniejącej zawartości w slocie {slot}.",
"@p_overwrite_slot_desc": {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Yubico.
* Copyright (C) 2023,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,9 +28,17 @@ import '../features.dart' as features;
import '../icon_provider/icon_pack_dialog.dart';
import '../keys.dart' as keys;
import '../models.dart';
import 'manage_password_dialog.dart';
import 'utils.dart';
bool oathShowActionNotifier(DeviceInfo? info) {
if (info == null) {
return false;
}
final (fipsCapable, fipsApproved) = info.getFipsStatus(Capability.oath);
return fipsCapable && !fipsApproved;
}
Widget oathBuildActions(
BuildContext context,
DevicePath devicePath,
@ -63,6 +71,10 @@ Widget oathBuildActions(
enabled = true;
}
final colors = Theme.of(context).buttonTheme.colorScheme ??
Theme.of(context).colorScheme;
final alertIcon = Icon(Symbols.warning_amber, color: colors.tertiary);
return Column(
children: [
ActionListSection(l10n.s_setup, children: [
@ -103,13 +115,10 @@ Widget oathBuildActions(
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
subtitle: l10n.l_password_protection,
icon: const Icon(Symbols.password),
trailing: fipsCapable && !fipsApproved ? alertIcon : null,
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(devicePath, oathState),
);
managePassword(context, ref, devicePath, oathState);
}),
]),
],

View File

@ -167,7 +167,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
final hasFeature = ref.watch(featureProvider);
final hasActions = hasFeature(features.actions);
final searchText = searchController.text;
final deviceInfo =
ref.watch(currentDeviceDataProvider.select((s) => s.valueOrNull?.info));
Future<void> onFileDropped(File file) async {
final qrScanner = ref.read(qrScannerProvider);
if (qrScanner != null) {
@ -186,21 +187,36 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
if (numCreds == 0) {
return MessagePage(
actionsBuilder: (context, expanded) => [
if (!expanded)
ActionChip(
label: Text(l10n.s_add_account),
onPressed: () async {
await addOathAccount(
context,
ref,
widget.devicePath,
widget.oathState,
);
},
avatar: const Icon(Symbols.person_add_alt),
)
],
keyActionsBadge: oathShowActionNotifier(deviceInfo),
actionsBuilder: (context, expanded) {
final (fipsCapable, fipsApproved) =
deviceInfo?.getFipsStatus(Capability.oath) ?? (false, false);
return [
if (!expanded && (!fipsCapable || (fipsCapable && fipsApproved)))
ActionChip(
label: Text(l10n.s_add_account),
onPressed: () async {
await addOathAccount(
context,
ref,
widget.devicePath,
widget.oathState,
);
},
avatar: const Icon(Symbols.person_add_alt),
),
if (!expanded && fipsCapable && !fipsApproved)
ActionChip(
label: Text(l10n.s_set_password),
onPressed: () async {
await managePassword(
context, ref, widget.devicePath, widget.oathState);
},
avatar: const Icon(Symbols.person_add_alt),
)
];
},
title: l10n.s_accounts,
capabilities: const [Capability.oath],
key: keys.noAccountsView,
@ -225,6 +241,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
capabilities: const [Capability.oath],
centered: true,
delayedContent: true,
keyActionsBadge: oathShowActionNotifier(deviceInfo),
builder: (context, _) => const CircularProgressIndicator(),
);
}
@ -290,6 +307,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
alternativeTitle:
searchText != '' ? l10n.l_results_for(searchText) : null,
capabilities: const [Capability.oath],
keyActionsBadge: oathShowActionNotifier(deviceInfo),
keyActionsBuilder: hasActions
? (context) => oathBuildActions(
context,
@ -500,7 +518,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
},
child: PopupMenuButton(
constraints: const BoxConstraints.tightFor(),
tooltip: 'Select layout',
tooltip: l10n.s_select_layout,
popUpAnimationStyle:
AnimationStyle(duration: Duration.zero),
icon: Icon(

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -35,6 +35,7 @@ import '../models.dart';
import 'add_account_dialog.dart';
import 'add_account_page.dart';
import 'add_multi_account_page.dart';
import 'manage_password_dialog.dart';
/// Calculates the available space for issuer and account name.
///
@ -178,3 +179,11 @@ Future<void> addOathAccount(BuildContext context, WidgetRef ref,
);
}
}
Future<void> managePassword(BuildContext context, WidgetRef ref,
DevicePath devicePath, OathState oathState) async {
await showBlurDialog(
context: context,
builder: (context) => ManagePasswordDialog(devicePath, oathState),
);
}

View File

@ -99,6 +99,7 @@ const appListItem95 = Key('$_prefix.95.applistitem');
// SlotMetadata body keys
const slotMetadataKeyType = Key('$_prefix.slotMetadata.keyType');
const slotMetadataBiometrics = Key('$_prefix.slotMetadata.biometrics');
// CertInfo body keys
const certInfoKeyType = Key('$_prefix.certInfo.keyType');

View File

@ -28,9 +28,10 @@ class CertInfoTable extends ConsumerWidget {
final CertInfo? certInfo;
final SlotMetadata? metadata;
final bool alwaysIncludePrivate;
final bool supportsBio;
const CertInfoTable(this.certInfo, this.metadata,
{super.key, this.alwaysIncludePrivate = false});
{super.key, this.alwaysIncludePrivate = false, this.supportsBio = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -46,6 +47,16 @@ class CertInfoTable extends ConsumerWidget {
metadata.keyType.getDisplayName(l10n),
keys.slotMetadataKeyType
),
if (metadata != null &&
metadata.pinPolicy != PinPolicy.never &&
supportsBio)
l10n.s_biometrics: (
[PinPolicy.matchAlways, PinPolicy.matchOnce]
.contains(metadata.pinPolicy)
? l10n.s_enabled
: l10n.s_disabled,
keys.slotMetadataBiometrics
),
if (metadata == null && alwaysIncludePrivate)
l10n.s_private_key: (l10n.s_none, keys.slotMetadataKeyType),
if (certInfo != null) ...{

View File

@ -249,11 +249,13 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
FilterChip(
label: Text(l10n.s_allow_fingerprint),
selected: _allowMatch,
onSelected: (value) {
setState(() {
_allowMatch = value;
});
},
onSelected: _generating
? null
: (value) {
setState(() {
_allowMatch = value;
});
},
),
]),
Padding(

View File

@ -292,16 +292,6 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
),
],
),
if (!unsupportedKey && widget.showMatch)
FilterChip(
label: Text(l10n.s_allow_fingerprint),
selected: _allowMatch,
onSelected: (value) {
setState(() {
_allowMatch = value;
});
},
),
],
if (certInfo != null) ...[
Text(
@ -315,7 +305,25 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
140, // Needed for layout, adapt if text sizes changes
child: CertInfoTable(certInfo, null),
),
]
],
if (keyType != null && !unsupportedKey && widget.showMatch) ...[
Text(
l10n.s_options,
style: textTheme.bodyLarge,
),
Text(l10n.p_key_options_bio_desc),
FilterChip(
label: Text(l10n.s_allow_fingerprint),
selected: _allowMatch,
onSelected: _importing
? null
: (value) {
setState(() {
_allowMatch = value;
});
},
),
],
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -158,6 +158,7 @@ class _PivScreenState extends ConsumerState<PivScreen> {
selected.metadata,
alwaysIncludePrivate:
pivState.supportsMetadata,
supportsBio: pivState.supportsBio,
),
if (selected.certInfo == null)
const SizedBox(height: 16)

View File

@ -98,6 +98,7 @@ class SlotDialog extends ConsumerWidget {
certInfo,
metadata,
alwaysIncludePrivate: pivState.supportsMetadata,
supportsBio: pivState.supportsBio,
),
if (certInfo == null) const SizedBox(height: 16),
],

View File

@ -27,7 +27,9 @@ List<KeyType> getSupportedKeyTypes(Version version, bool isFips) => [
if (!isFips) KeyType.x25519,
],
KeyType.eccp256,
KeyType.eccp384,
if (version.isAtLeast(4, 0)) ...[
KeyType.eccp384,
]
];
PinPolicy getPinPolicy(SlotId slot, bool match) {

View File

@ -210,34 +210,34 @@ packages:
dependency: "direct main"
description:
name: crypto
sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8"
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
url: "https://pub.dev"
source: hosted
version: "3.0.4"
version: "3.0.5"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799"
sha256: "4939d89e580c36215e48a7de8fd92f22c79dcc3eb11fda84f3402b3b45aec663"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
version: "0.6.5"
custom_lint_builder:
dependency: "direct dev"
description:
name: custom_lint_builder
sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9
sha256: d9e5bb63ed52c1d006f5a1828992ba6de124c27a531e8fba0a31afffa81621b3
url: "https://pub.dev"
source: hosted
version: "0.6.4"
version: "0.6.5"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
url: "https://pub.dev"
source: hosted
version: "0.6.3"
version: "0.6.5"
dart_style:
dependency: transitive
description:
@ -282,10 +282,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "1375f8685ca6f0412effecc2db834757e9d0e3e055468053e563794b0755cdcd"
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
url: "https://pub.dev"
source: hosted
version: "8.1.1"
version: "8.1.2"
fixnum:
dependency: transitive
description:
@ -723,10 +723,10 @@ packages:
dependency: transitive
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
version: "0.28.0"
screen_retriever:
dependency: "direct main"
description:
@ -739,10 +739,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shared_preferences_android:
dependency: transitive
description:
@ -959,10 +959,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
url: "https://pub.dev"
source: hosted
version: "6.3.9"
version: "6.3.10"
url_launcher_ios:
dependency: transitive
description:
@ -1055,10 +1055,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.4"
version: "14.2.5"
watcher:
dependency: transitive
description:
@ -1141,5 +1141,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0-259.0.dev <4.0.0"
flutter: ">=3.22.0"
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"