mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Merge branch 'main' into dain/restricted-nfc
This commit is contained in:
commit
73f8ae5348
2
.github/workflows/env
vendored
2
.github/workflows/env
vendored
@ -1,2 +1,2 @@
|
||||
FLUTTER=3.24.0
|
||||
FLUTTER=3.24.1
|
||||
PYVER=3.12.5
|
||||
|
@ -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(
|
||||
|
@ -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>
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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
@ -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.",
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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);
|
||||
}),
|
||||
]),
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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) ...{
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -98,6 +98,7 @@ class SlotDialog extends ConsumerWidget {
|
||||
certInfo,
|
||||
metadata,
|
||||
alwaysIncludePrivate: pivState.supportsMetadata,
|
||||
supportsBio: pivState.supportsBio,
|
||||
),
|
||||
if (certInfo == null) const SizedBox(height: 16),
|
||||
],
|
||||
|
@ -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) {
|
||||
|
40
pubspec.lock
40
pubspec.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user