Merge branch main into adamve/nfc_activity_widget

This commit is contained in:
Adam Velebil 2024-08-25 10:54:31 +02:00
commit a82bcceecb
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
88 changed files with 2536 additions and 1149 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: set up JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'

View File

@ -1,2 +1,2 @@
FLUTTER=3.22.2
PYVER=3.12.4
FLUTTER=3.24.0
PYVER=3.12.5

View File

@ -52,11 +52,17 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
architecture: 'x64'
flutter-version: ${{ env.FLUTTER }}
- run: flutter config --enable-macos-desktop
- run: flutter --version
- name: Apply Flutter Patch
run: |
cd $FLUTTER_ROOT
git apply $GITHUB_WORKSPACE/macos_assemble.patch
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Run lints/tests
env:
SKIP: ${{ steps.cache-helper.outputs.cache-hit == 'true' && 'mypy,flake8,black,bandit' || ''}}

View File

@ -56,6 +56,7 @@ import com.yubico.authenticator.yubikit.NfcActivityDispatcher
import com.yubico.authenticator.yubikit.NfcActivityListener
import com.yubico.authenticator.yubikit.NfcActivityState
import com.yubico.authenticator.yubikit.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.YubiKitManager
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
@ -63,7 +64,14 @@ import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyManager
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager
import com.yubico.yubikit.core.Transport
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.smartcard.scp.KeyRef
import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
import com.yubico.yubikit.core.smartcard.scp.ScpKid
import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BinaryMessenger
@ -309,6 +317,25 @@ class MainActivity : FlutterFragmentActivity() {
return
}
// If NFC and FIPS check for SCP11b key
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
logger.debug("Checking for usable SCP11b key...")
deviceManager.scpKeyParams =
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
val scp = SecurityDomainSession(connection)
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
keyRef?.let {
val certs = scp.getCertificateBundle(it)
if (certs.isNotEmpty()) Scp11KeyParams(
keyRef,
certs[certs.size - 1].publicKey
) else null
}?.also {
logger.debug("Found SCP11b key: {}", keyRef)
}
}
}
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
logger.debug("Connected key supports: {}", supportedContexts)
if (!supportedContexts.contains(viewModel.appContext.value)) {
@ -465,7 +492,7 @@ class MainActivity : FlutterFragmentActivity() {
}
private val sharedPreferencesListener = OnSharedPreferenceChangeListener { _, key ->
if ( AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) {
if (AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) {
stopNfcDiscovery()
startNfcDiscovery()
}
@ -531,6 +558,7 @@ class MainActivity : FlutterFragmentActivity() {
}
result.success(true)
}
"hasCamera" -> {
val cameraService =
getSystemService(CAMERA_SERVICE) as CameraManager
@ -541,9 +569,11 @@ class MainActivity : FlutterFragmentActivity() {
}
)
}
"hasNfc" -> result.success(
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
)
"isNfcEnabled" -> {
val nfcAdapter = NfcAdapter.getDefaultAdapter(this@MainActivity)
@ -551,6 +581,7 @@ class MainActivity : FlutterFragmentActivity() {
nfcAdapter != null && nfcAdapter.isEnabled
)
}
"openNfcSettings" -> {
startActivity(Intent(ACTION_NFC_SETTINGS))
result.success(true)

View File

@ -24,6 +24,7 @@ import com.yubico.authenticator.MainViewModel
import com.yubico.authenticator.OperationContext
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
import com.yubico.yubikit.management.Capability
import org.slf4j.LoggerFactory
@ -46,6 +47,15 @@ class DeviceManager(
private val deviceListeners = HashSet<DeviceListener>()
val deviceInfo: Info?
get() = appViewModel.deviceInfo.value
var scpKeyParams: ScpKeyParams? = null
set(value) {
field = value
logger.debug("SCP params set to {}", value)
}
fun addDeviceListener(listener: DeviceListener) {
deviceListeners.add(listener)
}
@ -157,6 +167,7 @@ class DeviceManager(
fun setDeviceInfo(deviceInfo: Info?) {
appViewModel.setDeviceInfo(deviceInfo)
scpKeyParams = null
}
fun isUsbKeyConnected(): Boolean {

View File

@ -21,7 +21,7 @@ import com.yubico.yubikit.management.DeviceInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? =
private fun DeviceInfo.capabilitiesFor(transport: Transport): Int? =
when {
hasTransport(transport) -> getSupportedCapabilities(transport)
else -> null
@ -30,7 +30,7 @@ private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? =
@Serializable
data class Info(
@SerialName("config")
val config : Config,
val config: Config,
@SerialName("serial")
val serialNumber: Int?,
@SerialName("version")
@ -55,11 +55,19 @@ data class Info(
val supportedCapabilities: Capabilities,
@SerialName("fips_capable")
val fipsCapable: Int,
@SerialName("fips_approved")
val fipsApproved: Int,
@SerialName("reset_blocked")
val resetBlocked: Int,
) {
constructor(name: String, isNfc: Boolean, usbPid: Int?, deviceInfo: DeviceInfo) : this(
config = Config(deviceInfo.config),
serialNumber = deviceInfo.serialNumber,
version = Version(deviceInfo.version.major, deviceInfo.version.minor, deviceInfo.version.micro),
version = Version(
deviceInfo.version.major,
deviceInfo.version.minor,
deviceInfo.version.micro
),
formFactor = deviceInfo.formFactor.value,
isLocked = deviceInfo.isLocked,
isSky = deviceInfo.isSky,
@ -72,6 +80,8 @@ data class Info(
nfc = deviceInfo.capabilitiesFor(Transport.NFC),
usb = deviceInfo.capabilitiesFor(Transport.USB),
),
fipsCapable = deviceInfo.fipsCapable
fipsCapable = deviceInfo.fipsCapable,
fipsApproved = deviceInfo.fipsApproved,
resetBlocked = deviceInfo.resetBlocked,
)
}

View File

@ -22,7 +22,9 @@ val UnknownDevice = Info(
usbPid = null,
pinComplexity = false,
supportedCapabilities = Capabilities(),
fipsCapable = 0
fipsCapable = 0,
fipsApproved = 0,
resetBlocked = 0
)
fun unknownDeviceWithCapability(transport: Transport, bit: Int = 0) : Info {

View File

@ -26,7 +26,8 @@ enum class FidoActionDescription(private val value: Int) {
DeleteFingerprint(4),
RenameFingerprint(5),
RegisterFingerprint(6),
ActionFailure(7);
EnableEnterpriseAttestation(7),
ActionFailure(8);
val id: Int
get() = value + dialogDescriptionFidoIndex

View File

@ -42,6 +42,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.util.Result
import com.yubico.yubikit.fido.ctap.BioEnrollment
import com.yubico.yubikit.fido.ctap.ClientPin
import com.yubico.yubikit.fido.ctap.Config
import com.yubico.yubikit.fido.ctap.CredentialManagement
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment
@ -159,6 +160,8 @@ class FidoManager(
"cancelRegisterFingerprint" -> cancelRegisterFingerprint()
"enableEnterpriseAttestation" -> enableEnterpriseAttestation()
else -> throw NotImplementedError()
}
}
@ -603,6 +606,42 @@ class FidoManager(
).toString()
}
private suspend fun enableEnterpriseAttestation(): String =
connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession ->
try {
val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)
val clientPin = ClientPin(fidoSession, uvAuthProtocol)
val token = if (pinStore.hasPin()) {
clientPin.getPinToken(
pinStore.getPin(),
ClientPin.PIN_PERMISSION_ACFG,
null
)
} else null
val config = Config(fidoSession, uvAuthProtocol, token)
config.enableEnterpriseAttestation()
fidoViewModel.setSessionState(
Session(
fidoSession.info,
pinStore.hasPin(),
pinRetries
)
)
return@useSession JSONObject(
mapOf(
"success" to true,
)
).toString()
} catch (e: Exception) {
logger.error("Failed to enable enterprise attestation. ", e)
return@useSession JSONObject(
mapOf(
"success" to false,
)
).toString()
}
}
override fun onDisconnected() {
if (!resetHelper.inProgress) {

View File

@ -29,7 +29,8 @@ data class Options(
val credMgmt: Boolean,
val credentialMgmtPreview: Boolean,
val bioEnroll: Boolean?,
val alwaysUv: Boolean
val alwaysUv: Boolean,
val ep: Boolean?,
) {
constructor(infoData: InfoData) : this(
infoData.getOptionsBoolean("clientPin") == true,
@ -37,6 +38,7 @@ data class Options(
infoData.getOptionsBoolean("credentialMgmtPreview") == true,
infoData.getOptionsBoolean("bioEnroll"),
infoData.getOptionsBoolean("alwaysUv") == true,
infoData.getOptionsBoolean("ep"),
)
companion object {

View File

@ -56,6 +56,7 @@ import com.yubico.yubikit.core.smartcard.SW
import com.yubico.yubikit.core.smartcard.SmartCardConnection
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
import com.yubico.yubikit.core.util.Result
import com.yubico.yubikit.management.Capability
import com.yubico.yubikit.oath.CredentialData
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
@ -615,8 +616,10 @@ class OathManager(
* @param connection the device SmartCard connection
* @return a [YubiKitOathSession] which is unlocked or locked based on an internal parameter
*/
private fun getOathSession(connection: SmartCardConnection) : YubiKitOathSession {
val session = YubiKitOathSession(connection)
private fun getOathSession(connection: SmartCardConnection): YubiKitOathSession {
// If OATH is FIPS capable, and we have scpKeyParams, we should use them
val fips = (deviceManager.deviceInfo?.fipsCapable ?: 0) and Capability.OATH.bit != 0
val session = YubiKitOathSession(connection, if (fips) deviceManager.scpKeyParams else null)
if (!unlockOnConnect.compareAndSet(false, true)) {
tryToUnlockOathSession(session)

View File

@ -76,7 +76,9 @@ class SkyHelper(private val compatUtil: CompatUtil) {
usbPid = pid.value,
pinComplexity = false,
supportedCapabilities = Capabilities(usb = 0),
fipsCapable = 0
fipsCapable = 0,
fipsApproved = 0,
resetBlocked = 0
)
}

View File

@ -9,7 +9,7 @@ allprojects {
targetSdkVersion = 34
compileSdkVersion = 34
yubiKitVersion = "2.6.0"
yubiKitVersion = "2.7.0-alpha01"
junitVersion = "4.13.2"
mockitoVersion = "5.12.0"
}

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .base import RpcException, encode_bytes
from .base import RpcResponse, RpcException, encode_bytes
from .device import RootNode
from queue import Queue
@ -80,7 +80,7 @@ def _handle_incoming(event, recv, error, cmd_queue):
def process(
send: Callable[[Dict], None],
recv: Callable[[], Dict],
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], Dict],
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], RpcResponse],
) -> None:
def error(status: str, message: str, body: Dict = {}):
send(dict(kind="error", status=status, message=message, body=body))
@ -88,8 +88,8 @@ def process(
def signal(status: str, body: Dict = {}):
send(dict(kind="signal", status=status, body=body))
def success(body: Dict):
send(dict(kind="success", body=body))
def success(response: RpcResponse):
send(dict(kind="success", body=response.body, flags=response.flags))
event = Event()
cmd_queue: Queue = Queue(1)

View File

@ -27,6 +27,12 @@ def encode_bytes(value: bytes) -> str:
decode_bytes = bytes.fromhex
class RpcResponse:
def __init__(self, body, flags=None):
self.body = body
self.flags = flags or []
class RpcException(Exception):
"""An exception that is returned as the result of an RPC command.i
@ -116,16 +122,20 @@ class RpcNode:
try:
if target:
traversed += [target[0]]
return self.get_child(target[0])(
response = self.get_child(target[0])(
action, target[1:], params, event, signal, traversed
)
if action in self.list_actions():
return self.get_action(action)(params, event, signal)
if action in self.list_children():
elif action in self.list_actions():
response = self.get_action(action)(params, event, signal)
elif action in self.list_children():
traversed += [action]
return self.get_child(action)(
response = self.get_child(action)(
"get", [], params, event, signal, traversed
)
if isinstance(response, RpcResponse):
return response
return RpcResponse(response)
except ChildResetException as e:
self._close_child()
raise StateResetException(e.message, traversed)

View File

@ -31,18 +31,21 @@ from ykman.base import PID
from ykman.device import scan_devices, list_all_devices
from ykman.diagnostics import get_diagnostics
from ykman.logging import set_log_level
from yubikit.core import TRANSPORT
from yubikit.core import TRANSPORT, NotSupportedError
from yubikit.core.smartcard import SmartCardConnection, ApduError, SW
from yubikit.core.smartcard.scp import Scp11KeyParams
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.support import get_name, read_info
from yubikit.management import CAPABILITY
from yubikit.securitydomain import SecurityDomainSession
from yubikit.logging import LOG_LEVEL
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 cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from hashlib import sha256
from dataclasses import asdict
from typing import Mapping, Tuple
@ -255,12 +258,24 @@ class AbstractDeviceNode(RpcNode):
super().__init__()
self._device = device
self._info = info
self._data = None
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
response = super().__call__(*args, **kwargs)
if "device_info" in response.flags:
# Clear DeviceInfo cache
self._info = None
self._data = None
# Make sure any child node is re-opened after this,
# as enabled applications may have changed
super().close()
return response
except (SmartcardException, OSError):
logger.exception("Device error")
self._child = None
name = self._child_name
self._child_name = None
@ -273,6 +288,14 @@ class AbstractDeviceNode(RpcNode):
logger.exception(f"Unable to create child {name}")
raise NoSuchNodeException(name)
def get_data(self):
if not self._data:
self._data = self._refresh_data()
return self._data
def _refresh_data(self):
...
def _read_data(self, conn):
pid = self._device.pid
self._info = read_info(conn, pid)
@ -293,7 +316,7 @@ class UsbDeviceNode(AbstractDeviceNode):
connection = self._device.open_connection(conn_type)
return ConnectionNode(self._device, connection, self._info)
def get_data(self):
def _refresh_data(self):
for conn_type in (SmartCardConnection, OtpConnection, FidoConnection):
if self._supports_connection(conn_type):
try:
@ -332,7 +355,7 @@ class _ReaderObserver(CardObserver):
def __init__(self, device):
self.device = device
self.card = None
self.data = None
self.needs_refresh = True
def update(self, observable, actions):
added, removed = actions
@ -343,7 +366,7 @@ class _ReaderObserver(CardObserver):
break
else:
self.card = None
self.data = None
self.needs_refresh = True
logger.debug(f"NFC card: {self.card}")
@ -359,18 +382,24 @@ class ReaderDeviceNode(AbstractDeviceNode):
super().close()
def get_data(self):
if self._observer.data is None:
card = self._observer.card
if card is None:
return dict(present=False, status="no-card")
try:
with self._device.open_connection(SmartCardConnection) as conn:
self._observer.data = dict(self._read_data(conn), present=True)
except NoCardException:
return dict(present=False, status="no-card")
except ValueError:
self._observer.data = dict(present=False, status="unknown-device")
return self._observer.data
if self._observer.needs_refresh:
self._data = None
return super().get_data()
def _refresh_data(self):
card = self._observer.card
if card is None:
return dict(present=False, status="no-card")
try:
with self._device.open_connection(SmartCardConnection) as conn:
data = dict(self._read_data(conn), present=True)
self._observer.needs_refresh = False
return data
except NoCardException:
return dict(present=False, status="no-card")
except ValueError:
self._observer.needs_refresh = False
return dict(present=False, status="unknown-device")
@action(closes_child=False)
def get(self, params, event, signal):
@ -381,7 +410,7 @@ class ReaderDeviceNode(AbstractDeviceNode):
try:
connection = self._device.open_connection(SmartCardConnection)
info = read_info(connection)
return ConnectionNode(self._device, connection, info)
return ScpConnectionNode(self._device, connection, info)
except (ValueError, SmartcardException, EstablishContextException) as e:
logger.warning("Error opening connection", exc_info=True)
raise ConnectionException(self._device.fingerprint, "ccid", e)
@ -436,33 +465,36 @@ class ConnectionNode(RpcNode):
self._info = read_info(self._connection, self._device.pid)
return dict(version=self._info.version, serial=self._info.serial)
def _init_child_node(self, child_cls, capability=CAPABILITY(0)):
return child_cls(self._connection)
@child(
condition=lambda self: self._transport == TRANSPORT.USB
or isinstance(self._connection, SmartCardConnection)
)
def management(self):
return ManagementNode(self._connection)
return self._init_child_node(ManagementNode)
@child(
condition=lambda self: isinstance(self._connection, SmartCardConnection)
and CAPABILITY.OATH in self.capabilities
)
def oath(self):
return OathNode(self._connection)
return self._init_child_node(OathNode, CAPABILITY.OATH)
@child(
condition=lambda self: isinstance(self._connection, SmartCardConnection)
and CAPABILITY.PIV in self.capabilities
)
def piv(self):
return PivNode(self._connection)
return self._init_child_node(PivNode, CAPABILITY.PIV)
@child(
condition=lambda self: isinstance(self._connection, FidoConnection)
and CAPABILITY.FIDO2 in self.capabilities
)
def ctap2(self):
return Ctap2Node(self._connection)
return self._init_child_node(Ctap2Node)
@child(
condition=lambda self: CAPABILITY.OTP in self.capabilities
@ -479,4 +511,30 @@ class ConnectionNode(RpcNode):
)
)
def yubiotp(self):
return YubiOtpNode(self._connection)
return self._init_child_node(YubiOtpNode)
class ScpConnectionNode(ConnectionNode):
def __init__(self, device, connection, info):
super().__init__(device, connection, info)
self.fips_capable = info.fips_capable
self.scp_params = None
try:
scp = SecurityDomainSession(connection)
for ref in scp.get_key_information().keys():
if ref.kid == 0x13:
chain = scp.get_certificate_bundle(ref)
if chain:
pub_key = chain[-1].public_key()
assert isinstance(pub_key, EllipticCurvePublicKey) # nosec
self.scp_params = Scp11KeyParams(ref, pub_key)
break
except NotSupportedError:
pass
def _init_child_node(self, child_cls, capability=CAPABILITY(0)):
if capability in self.fips_capable:
return child_cls(self._connection, self.scp_params)
return child_cls(self._connection)

View File

@ -13,6 +13,7 @@
# limitations under the License.
from .base import (
RpcResponse,
RpcNode,
action,
child,
@ -22,7 +23,7 @@ from .base import (
PinComplexityException,
)
from fido2.ctap import CtapError
from fido2.ctap2 import Ctap2, ClientPin
from fido2.ctap2 import Ctap2, ClientPin, Config
from fido2.ctap2.credman import CredentialManagement
from fido2.ctap2.bio import BioEnrollment, FPBioEnrollment, CaptureError
from fido2.pcsc import CtapPcscDevice
@ -189,7 +190,7 @@ class Ctap2Node(RpcNode):
raise InactivityException()
self._info = self.ctap.get_info()
self._token = None
return dict()
return RpcResponse(dict(), ["device_info"])
@action(condition=lambda self: self._info.options["clientPin"])
def unlock(self, params, event, signal):
@ -199,6 +200,8 @@ class Ctap2Node(RpcNode):
permissions |= ClientPin.PERMISSION.CREDENTIAL_MGMT
if BioEnrollment.is_supported(self._info):
permissions |= ClientPin.PERMISSION.BIO_ENROLL
if Config.is_supported(self._info):
permissions |= ClientPin.PERMISSION.AUTHENTICATOR_CFG
try:
if permissions:
self._token = self.client_pin.get_pin_token(pin, permissions)
@ -224,10 +227,18 @@ class Ctap2Node(RpcNode):
params.pop("new_pin"),
)
self._info = self.ctap.get_info()
return dict()
return RpcResponse(dict(), ["device_info"])
except CtapError as e:
return _handle_pin_error(e, self.client_pin)
@action(condition=lambda self: Config.is_supported(self._info))
def enable_ep_attestation(self, params, event, signal):
if self._info.options["clientPin"] and not self._token:
raise AuthRequiredException()
config = Config(self.ctap, self.client_pin.protocol, self._token)
config._call(Config.CMD.ENABLE_ENTERPRISE_ATT)
return dict()
@child(condition=lambda self: BioEnrollment.is_supported(self._info))
def fingerprints(self):
if not self._token:

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .base import RpcNode, action
from .base import RpcResponse, RpcNode, action
from yubikit.core import require_version, NotSupportedError, TRANSPORT, Connection
from yubikit.core.smartcard import SmartCardConnection
from yubikit.core.otp import OtpConnection
@ -28,10 +28,10 @@ logger = logging.getLogger(__name__)
class ManagementNode(RpcNode):
def __init__(self, connection):
def __init__(self, connection, scp_params=None):
super().__init__()
self._connection_type: Type[Connection] = type(connection)
self.session = ManagementSession(connection)
self.session = ManagementSession(connection, scp_params)
def get_data(self):
try:
@ -90,7 +90,7 @@ class ManagementNode(RpcNode):
if reboot:
enabled = config.enabled_capabilities.get(TRANSPORT.USB)
self._await_reboot(serial, enabled)
return dict()
return RpcResponse(dict(), ["device_info"])
@action
def set_mode(self, params, event, signal):
@ -106,4 +106,4 @@ class ManagementNode(RpcNode):
)
def device_reset(self, params, event, signal):
self.session.device_reset()
return dict()
return RpcResponse(dict(), ["device_info"])

View File

@ -13,6 +13,7 @@
# limitations under the License.
from .base import (
RpcResponse,
RpcNode,
action,
child,
@ -77,9 +78,9 @@ class OathNode(RpcNode):
logger.warning("Failed to unwrap access key", exc_info=True)
return None
def __init__(self, connection):
def __init__(self, connection, scp_params=None):
super().__init__()
self.session = OathSession(connection)
self.session = OathSession(connection, scp_params)
self._key_verifier = None
if self.session.locked:
@ -193,7 +194,7 @@ class OathNode(RpcNode):
self.session.set_key(key)
self._set_key_verifier(key)
remember &= self._remember_key(key if remember else None)
return dict(remembered=remember)
return RpcResponse(dict(remembered=remember), ["device_info"])
@action(condition=lambda self: self.session.has_key)
def unset_key(self, params, event, signal):
@ -207,7 +208,7 @@ class OathNode(RpcNode):
self.session.reset()
self._key_verifier = None
self._remember_key(None)
return dict()
return RpcResponse(dict(), ["device_info"])
@child
def accounts(self):

View File

@ -13,6 +13,7 @@
# limitations under the License.
from .base import (
RpcResponse,
RpcNode,
action,
child,
@ -91,9 +92,9 @@ def _handle_pin_puk_error(e):
class PivNode(RpcNode):
def __init__(self, connection):
def __init__(self, connection, scp_params=None):
super().__init__()
self.session = PivSession(connection)
self.session = PivSession(connection, scp_params)
self._pivman_data = get_pivman_data(self.session)
self._authenticated = False
@ -212,7 +213,7 @@ class PivNode(RpcNode):
store_key = params.pop("store_key", False)
pivman_set_mgm_key(self.session, key, key_type, False, store_key)
self._pivman_data = get_pivman_data(self.session)
return dict()
return RpcResponse(dict(), ["device_info"])
@action
def change_pin(self, params, event, signal):
@ -220,9 +221,9 @@ class PivNode(RpcNode):
new_pin = params.pop("new_pin")
try:
pivman_change_pin(self.session, old_pin, new_pin)
return RpcResponse(dict(), ["device_info"])
except Exception as e:
_handle_pin_puk_error(e)
return dict()
@action
def change_puk(self, params, event, signal):
@ -230,9 +231,9 @@ class PivNode(RpcNode):
new_puk = params.pop("new_puk")
try:
self.session.change_puk(old_puk, new_puk)
return RpcResponse(dict(), ["device_info"])
except Exception as e:
_handle_pin_puk_error(e)
return dict()
@action
def unblock_pin(self, params, event, signal):
@ -240,16 +241,16 @@ class PivNode(RpcNode):
new_pin = params.pop("new_pin")
try:
self.session.unblock_pin(puk, new_pin)
return RpcResponse(dict(), ["device_info"])
except Exception as e:
_handle_pin_puk_error(e)
return dict()
@action
def reset(self, params, event, signal):
self.session.reset()
self._authenticated = False
self._pivman_data = get_pivman_data(self.session)
return dict()
return RpcResponse(dict(), ["device_info"])
@child
def slots(self):
@ -266,9 +267,11 @@ class PivNode(RpcNode):
return dict(
status=True,
password=password is not None,
key_type=KEY_TYPE.from_public_key(private_key.public_key())
if private_key
else None,
key_type=(
KEY_TYPE.from_public_key(private_key.public_key())
if private_key
else None
),
cert_info=_get_cert_info(certificate),
)
except InvalidPasswordError:
@ -413,9 +416,11 @@ class SlotNode(RpcNode):
id=f"{int(self.slot):02x}",
name=self.slot.name,
metadata=_metadata_dict(self.metadata),
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if self.certificate
else None,
certificate=(
self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if self.certificate
else None
),
)
@action(condition=lambda self: self.certificate or self.metadata)
@ -492,16 +497,20 @@ class SlotNode(RpcNode):
return dict(
metadata=_metadata_dict(metadata),
public_key=private_key.public_key()
.public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
.decode()
if private_key
else None,
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if certs
else None,
public_key=(
private_key.public_key()
.public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
.decode()
if private_key
else None
),
certificate=(
self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if certs
else None
),
)
@action

View File

@ -40,9 +40,9 @@ _FAIL_MSG = (
class YubiOtpNode(RpcNode):
def __init__(self, connection):
def __init__(self, connection, scp_params=None):
super().__init__()
self.session = YubiOtpSession(connection)
self.session = YubiOtpSession(connection, scp_params)
def get_data(self):
state = self.session.get_config_state()

359
helper/poetry.lock generated
View File

@ -28,63 +28,78 @@ testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-ch
[[package]]
name = "cffi"
version = "1.16.0"
version = "1.17.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
{file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
{file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
{file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
{file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
{file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
{file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
{file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
{file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
{file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
{file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
{file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
{file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
{file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
{file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
{file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
{file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
{file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
{file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
{file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
]
[package.dependencies]
@ -117,43 +132,38 @@ files = [
[[package]]
name = "cryptography"
version = "42.0.8"
version = "43.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
]
[package.dependencies]
@ -166,18 +176,18 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "exceptiongroup"
version = "1.2.1"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
@ -202,13 +212,13 @@ pcsc = ["pyscard (>=1.9,<3)"]
[[package]]
name = "importlib-metadata"
version = "8.0.0"
version = "8.2.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
{file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"},
{file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"},
]
[package.dependencies]
@ -221,21 +231,21 @@ test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "p
[[package]]
name = "importlib-resources"
version = "6.4.0"
version = "6.4.2"
description = "Read resources from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
{file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
{file = "importlib_resources-6.4.2-py3-none-any.whl", hash = "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c"},
{file = "importlib_resources-6.4.2.tar.gz", hash = "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a"},
]
[package.dependencies]
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
[[package]]
name = "iniconfig"
@ -286,21 +296,21 @@ testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytes
[[package]]
name = "jaraco-functools"
version = "4.0.1"
version = "4.0.2"
description = "Functools like those found in stdlib"
optional = false
python-versions = ">=3.8"
files = [
{file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"},
{file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"},
{file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"},
{file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "jeepney"
@ -319,13 +329,13 @@ trio = ["async_generator", "trio"]
[[package]]
name = "keyring"
version = "25.2.1"
version = "25.3.0"
description = "Store and access your passwords safely."
optional = false
python-versions = ">=3.8"
files = [
{file = "keyring-25.2.1-py3-none-any.whl", hash = "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50"},
{file = "keyring-25.2.1.tar.gz", hash = "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b"},
{file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"},
{file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"},
]
[package.dependencies]
@ -340,8 +350,8 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras]
completion = ["shtab (>=1.1.0)"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "macholib"
@ -359,13 +369,13 @@ altgraph = ">=0.17"
[[package]]
name = "more-itertools"
version = "10.3.0"
version = "10.4.0"
description = "More routines for operating on iterables, beyond itertools"
optional = false
python-versions = ">=3.8"
files = [
{file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"},
{file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"},
{file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"},
{file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"},
]
[[package]]
@ -381,44 +391,44 @@ files = [
[[package]]
name = "mypy"
version = "1.10.1"
version = "1.11.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
{file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
{file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
{file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
{file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
{file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
{file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
{file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
{file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
{file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
{file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
{file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
{file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
{file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
{file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
{file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
{file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
{file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
{file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
{file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
{file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
{file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
{file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
{file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
{file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
{file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
{file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.1.0"
typing-extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
@ -584,23 +594,23 @@ files = [
[[package]]
name = "pyinstaller"
version = "6.8.0"
version = "6.10.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
python-versions = "<3.13,>=3.8"
python-versions = "<3.14,>=3.8"
files = [
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
{file = "pyinstaller-6.10.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d60fb22859e11483af735aec115fdde09467cdbb29edd9844839f2c920b748c0"},
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:46d75359668993ddd98630a3669dc5249f3c446e35239b43bc7f4155bc574748"},
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_i686.whl", hash = "sha256:3398a98fa17d47ccb31f8779ecbdacec025f7adb2f22757a54b706ac8b4fe906"},
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e9989f354ae4ed8a3bec7bdb37ae0d170751d6520e500f049c7cd0632d31d5c3"},
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b7c90c91921b3749083115b28f30f40abf2bb481ceff196d2b2ce0eaa2b3d429"},
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf876d7d93b8b4f28d1ad57fa24645cf43119c79e985dd5e5f7a801245e6f53"},
{file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:db05e3f2f10f9f78c56f1fb163d9cb453433429fe4281218ebaf1ebfd39ba942"},
{file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28eca3817f176fdc19747e1afcf434f13bb9f17a644f611be2c5a61b1f498ed7"},
{file = "pyinstaller-6.10.0-py3-none-win32.whl", hash = "sha256:703e041718987e46ba0568a2c71ecf2459fddef57cf9edf3efeed4a53e3dae3f"},
{file = "pyinstaller-6.10.0-py3-none-win_amd64.whl", hash = "sha256:95b55966e563e8b8f31a43882aea10169e9a11fdf38e626d86a2907b640c0701"},
{file = "pyinstaller-6.10.0-py3-none-win_arm64.whl", hash = "sha256:308e0a8670c9c9ac0cebbf1bbb492e71b6675606f2ec78bc4adfc830d209e087"},
{file = "pyinstaller-6.10.0.tar.gz", hash = "sha256:143840f8056ff7b910bf8f16f6cd92cc10a6c2680bb76d0a25d558d543d21270"},
]
[package.dependencies]
@ -609,7 +619,7 @@ importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2024.6"
pyinstaller-hooks-contrib = ">=2024.8"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
@ -619,13 +629,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2024.7"
version = "2024.8"
description = "Community maintained hooks for PyInstaller"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
{file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"},
{file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"},
]
[package.dependencies]
@ -662,13 +672,13 @@ pyro = ["Pyro"]
[[package]]
name = "pytest"
version = "8.2.2"
version = "8.3.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
]
[package.dependencies]
@ -676,7 +686,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2.0"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
@ -707,13 +717,13 @@ files = [
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
version = "0.2.3"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
optional = false
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
]
[[package]]
@ -733,18 +743,19 @@ jeepney = ">=0.6"
[[package]]
name = "setuptools"
version = "70.2.0"
version = "72.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
{file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
{file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"},
{file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"},
]
[package.extras]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "tomli"
@ -800,13 +811,13 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
[[package]]
name = "zipp"
version = "3.19.2"
version = "3.20.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
{file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"},
{file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"},
]
[package.extras]

View File

@ -94,9 +94,10 @@ class RpcShell(cmd.Cmd):
cmd = target.pop() if target else ""
node = self.get_node(target)
if node:
names = [n + "/" for n in node.get("children", [])]
body = node.get("body", {})
names = [n + "/" for n in body.get("children", [])]
if not nodes_only:
actions = node.get("actions", [])
actions = body.get("actions", [])
if "get" in actions:
actions.remove("get")
names += actions
@ -104,10 +105,10 @@ class RpcShell(cmd.Cmd):
return res
return []
def completedefault(self, cmd, text, *args):
def completedefault(self, cmd, text, *args): # type: ignore
return self.completepath(text)
def completenames(self, cmd, text, *ignored):
def completenames(self, cmd, text, *ignored): # type: ignore
return self.completepath(text)
def emptyline(self):

View File

@ -161,6 +161,29 @@ class _FidoStateNotifier extends FidoStateNotifier {
throw decodedException;
}
}
@override
Future<void> enableEnterpriseAttestation() async {
try {
final response = jsonDecode(await _methods.invokeMethod(
'enableEnterpriseAttestation',
));
if (response['success'] == true) {
_log.debug('Enterprise attestation enabled');
}
} on PlatformException catch (pe) {
var decodedException = pe.decode();
if (decodedException is CancellationException) {
_log.debug('User cancelled unlock FIDO operation');
throw decodedException;
}
_log.debug(
'Platform exception during enable enterprise attestation: $pe');
rethrow;
}
}
}
final androidFingerprintProvider = AsyncNotifierProvider.autoDispose

View File

@ -64,6 +64,8 @@ enum _DDesc {
fidoDeleteCredential,
fidoDeleteFingerprint,
fidoRenameFingerprint,
fidoRegisterFingerprint,
fidoEnableEnterpriseAttestation,
fidoActionFailure,
// Others
invalid;
@ -89,7 +91,9 @@ enum _DDesc {
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
dialogDescriptionFidoIndex + 6: fidoActionFailure,
dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint,
dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation,
dialogDescriptionFidoIndex + 8: fidoActionFailure,
}[id] ??
_DDesc.invalid;
}

View File

@ -32,7 +32,7 @@ Future<T?> showBlurDialog<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,
RouteSettings? routeSettings,
Color barrierColor = const Color(0x00cccccc),
Color barrierColor = const Color(0x33000000),
}) async =>
await showGeneralDialog<T>(
context: context,

View File

@ -9,9 +9,9 @@ part of 'models.dart';
_$KeyCustomizationImpl _$$KeyCustomizationImplFromJson(
Map<String, dynamic> json) =>
_$KeyCustomizationImpl(
serial: json['serial'] as int,
serial: (json['serial'] as num).toInt(),
name: json['name'] as String?,
color: const _ColorConverter().fromJson(json['color'] as int?),
color: const _ColorConverter().fromJson((json['color'] as num?)?.toInt()),
);
Map<String, dynamic> _$$KeyCustomizationImplToJson(

View File

@ -30,8 +30,10 @@ class AppListItem<T> extends ConsumerStatefulWidget {
final String? semanticTitle;
final Widget? trailing;
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
final Widget Function(BuildContext context)? itemBuilder;
final Intent? tapIntent;
final Intent? doubleTapIntent;
final Color? tileColor;
final bool selected;
const AppListItem(
@ -43,8 +45,10 @@ class AppListItem<T> extends ConsumerStatefulWidget {
this.subtitle,
this.trailing,
this.buildPopupActions,
this.itemBuilder,
this.tapIntent,
this.doubleTapIntent,
this.tileColor,
this.selected = false,
});
@ -78,7 +82,7 @@ class _AppListItemState<T> extends ConsumerState<AppListItem> {
item: widget.item,
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(48),
borderRadius: BorderRadius.circular(16),
onSecondaryTapDown: buildPopupActions == null
? null
: (details) {
@ -118,57 +122,62 @@ class _AppListItemState<T> extends ConsumerState<AppListItem> {
: () {
Actions.invoke(context, doubleTapIntent);
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
const SizedBox(height: 64),
ListTile(
mouseCursor:
widget.tapIntent != null ? SystemMouseCursors.click : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(48)),
selectedTileColor: colorScheme.secondaryContainer,
selectedColor: colorScheme.onSecondaryContainer,
selected: widget.selected,
leading: widget.leading,
title: subtitle == null
// We use SizedBox to fill entire space
? SizedBox(
height: 48,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
),
)
: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: trailing == null
? null
: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: trailing,
),
),
],
),
child: widget.itemBuilder != null
? widget.itemBuilder!.call(context)
: Stack(
alignment: AlignmentDirectional.center,
children: [
const SizedBox(height: 64),
ListTile(
mouseCursor: widget.tapIntent != null
? SystemMouseCursors.click
: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
selectedTileColor: colorScheme.secondaryContainer,
selectedColor: colorScheme.onSecondaryContainer,
tileColor: widget.tileColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
selected: widget.selected,
leading: widget.leading,
title: subtitle == null
// We use SizedBox to fill entire space
? SizedBox(
height: 48,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
),
)
: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: trailing == null
? null
: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: trailing,
),
),
],
),
),
),
);

View File

@ -22,6 +22,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/state.dart';
import '../../management/models.dart';
@ -29,18 +30,29 @@ import '../../widgets/delayed_visibility.dart';
import '../../widgets/file_drop_target.dart';
import '../message.dart';
import '../shortcuts.dart';
import '../state.dart';
import 'fs_dialog.dart';
import 'keys.dart';
import 'navigation.dart';
final _navigationProvider = StateNotifierProvider<_NavigationProvider, bool>(
(ref) => _NavigationProvider());
final _navigationVisibilityProvider =
StateNotifierProvider<_VisibilityNotifier, bool>((ref) =>
_VisibilityNotifier('NAVIGATION_VISIBILITY', ref.watch(prefProvider)));
class _NavigationProvider extends StateNotifier<bool> {
_NavigationProvider() : super(true);
final _detailViewVisibilityProvider =
StateNotifierProvider<_VisibilityNotifier, bool>((ref) =>
_VisibilityNotifier('DETAIL_VIEW_VISIBILITY', ref.watch(prefProvider)));
class _VisibilityNotifier extends StateNotifier<bool> {
final String _key;
final SharedPreferences _prefs;
_VisibilityNotifier(this._key, this._prefs)
: super(_prefs.getBool(_key) ?? true);
void toggleExpanded() {
state = !state;
final newValue = !state;
state = newValue;
_prefs.setBool(_key, newValue);
}
}
@ -308,14 +320,17 @@ class _AppPageState extends ConsumerState<AppPage> {
Widget? _buildAppBarTitle(
BuildContext context, bool hasRail, bool hasManage, bool fullyExpanded) {
final showNavigation = ref.watch(_navigationProvider);
final showNavigation = ref.watch(_navigationVisibilityProvider);
final showDetailView = ref.watch(_detailViewVisibilityProvider);
EdgeInsets padding;
if (fullyExpanded) {
padding = EdgeInsets.only(left: showNavigation ? 280 : 72, right: 320);
padding = EdgeInsets.only(
left: showNavigation ? 280 : 72, right: showDetailView ? 320 : 0.0);
} else if (!hasRail && hasManage) {
padding = const EdgeInsets.only(right: 320);
} else if (hasRail && hasManage) {
padding = const EdgeInsets.only(left: 72, right: 320);
padding = EdgeInsets.only(left: 72, right: showDetailView ? 320 : 0.0);
} else if (hasRail && !hasManage) {
padding = const EdgeInsets.only(left: 72);
} else {
@ -344,21 +359,23 @@ class _AppPageState extends ConsumerState<AppPage> {
}
Widget _buildMainContent(BuildContext context, bool expanded) {
final actions = widget.actionsBuilder?.call(context, expanded) ?? [];
final showDetailView = ref.watch(_detailViewVisibilityProvider);
final actions =
widget.actionsBuilder?.call(context, expanded && showDetailView) ?? [];
final content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: widget.centered
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
children: [
widget.builder(context, expanded),
widget.builder(context, expanded && showDetailView),
if (actions.isNotEmpty)
Align(
alignment:
widget.centered ? Alignment.center : Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
top: 16, bottom: 0, left: 16, right: 16),
top: 16, bottom: 0, left: 18, right: 18),
child: Wrap(
spacing: 8,
runSpacing: 4,
@ -369,7 +386,7 @@ class _AppPageState extends ConsumerState<AppPage> {
if (widget.footnote != null)
Padding(
padding:
const EdgeInsets.only(bottom: 16, top: 33, left: 16, right: 16),
const EdgeInsets.only(bottom: 16, top: 33, left: 18, right: 18),
child: Opacity(
opacity: 0.6,
child: Text(
@ -399,7 +416,7 @@ class _AppPageState extends ConsumerState<AppPage> {
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 16.0, right: 16.0, bottom: 24.0, top: 4.0),
left: 18.0, right: 18.0, bottom: 24.0, top: 4.0),
child: _buildTitle(context),
),
),
@ -452,7 +469,7 @@ class _AppPageState extends ConsumerState<AppPage> {
child: Padding(
key: _sliverTitleWrapperGlobalKey,
padding: const EdgeInsets.only(
left: 16.0, right: 16.0, bottom: 12.0, top: 4.0),
left: 18.0, right: 18.0, bottom: 12.0, top: 4.0),
child: _buildTitle(context),
),
),
@ -499,7 +516,8 @@ class _AppPageState extends ConsumerState<AppPage> {
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
final l10n = AppLocalizations.of(context)!;
final fullyExpanded = !hasDrawer && hasRail && hasManage;
final showNavigation = ref.watch(_navigationProvider);
final showNavigation = ref.watch(_navigationVisibilityProvider);
final showDetailView = ref.watch(_detailViewVisibilityProvider);
final hasDetailsOrKeyActions =
widget.detailViewBuilder != null || widget.keyActionsBuilder != null;
var body = _buildMainContent(context, hasManage);
@ -518,187 +536,211 @@ class _AppPageState extends ConsumerState<AppPage> {
);
}
if (hasRail || hasManage) {
body = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (hasRail && (!fullyExpanded || !showNavigation))
SizedBox(
width: 72,
child: _VisibilityListener(
targetKey: _navKey,
controller: _navController,
child: SingleChildScrollView(
child: NavigationContent(
key: _navKey,
shouldPop: false,
extended: false,
body = GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTap: () {
Actions.invoke(context, const EscapeIntent());
FocusManager.instance.primaryFocus?.unfocus();
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (hasRail && (!fullyExpanded || !showNavigation))
SizedBox(
width: 72,
child: _VisibilityListener(
targetKey: _navKey,
controller: _navController,
child: SingleChildScrollView(
child: NavigationContent(
key: _navKey,
shouldPop: false,
extended: false,
),
),
),
),
),
if (fullyExpanded && showNavigation)
SizedBox(
width: 280,
child: _VisibilityListener(
controller: _navController,
targetKey: _navExpandedKey,
child: SingleChildScrollView(
child: Material(
type: MaterialType.transparency,
child: NavigationContent(
key: _navExpandedKey,
shouldPop: false,
extended: true,
if (fullyExpanded && showNavigation)
SizedBox(
width: 280,
child: _VisibilityListener(
controller: _navController,
targetKey: _navExpandedKey,
child: SingleChildScrollView(
child: Material(
type: MaterialType.transparency,
child: NavigationContent(
key: _navExpandedKey,
shouldPop: false,
extended: true,
),
),
),
)),
const SizedBox(width: 8),
Expanded(child: body),
if (hasManage &&
!hasDetailsOrKeyActions &&
widget.capabilities != null &&
widget.capabilities?.first != Capability.u2f)
// Add a placeholder for the Manage/Details column. Exceptions are:
// - the "Security Key" because it does not have any actions/details.
// - pages without Capabilities
const SizedBox(width: 336), // simulate column
if (hasManage && hasDetailsOrKeyActions && showDetailView)
_VisibilityListener(
controller: _detailsController,
targetKey: _detailsViewGlobalKey,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
width: 320,
child: Column(
key: _detailsViewGlobalKey,
children: [
if (widget.detailViewBuilder != null)
widget.detailViewBuilder!(context),
if (widget.keyActionsBuilder != null)
widget.keyActionsBuilder!(context),
],
),
),
),
)),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTap: () {
Actions.invoke(context, const EscapeIntent());
},
child: Stack(children: [
Container(
color: Colors.transparent,
),
body
]),
)),
if (hasManage &&
!hasDetailsOrKeyActions &&
widget.capabilities != null &&
widget.capabilities?.first != Capability.u2f)
// Add a placeholder for the Manage/Details column. Exceptions are:
// - the "Security Key" because it does not have any actions/details.
// - pages without Capabilities
const SizedBox(width: 336), // simulate column
if (hasManage && hasDetailsOrKeyActions)
_VisibilityListener(
controller: _detailsController,
targetKey: _detailsViewGlobalKey,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
width: 320,
child: Column(
key: _detailsViewGlobalKey,
children: [
if (widget.detailViewBuilder != null)
widget.detailViewBuilder!(context),
if (widget.keyActionsBuilder != null)
widget.keyActionsBuilder!(context),
],
),
),
),
),
),
],
],
),
);
}
return Scaffold(
key: scaffoldGlobalKey,
appBar: AppBar(
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: ListenableBuilder(
listenable: _scrolledUnderController,
builder: (context, child) {
final visible = _scrolledUnderController.someIsScrolledUnder;
return AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: Container(
color: Theme.of(context).colorScheme.secondaryContainer,
height: 1.0,
),
);
},
appBar: _GestureDetectorAppBar(
onTap: () {
Actions.invoke(context, const EscapeIntent());
FocusManager.instance.primaryFocus?.unfocus();
},
appBar: AppBar(
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: ListenableBuilder(
listenable: _scrolledUnderController,
builder: (context, child) {
final visible = _scrolledUnderController.someIsScrolledUnder;
return AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: Container(
color: Theme.of(context).hoverColor,
height: 1.0,
),
);
},
),
),
),
scrolledUnderElevation: 0.0,
leadingWidth: hasRail ? 84 : null,
backgroundColor: Theme.of(context).colorScheme.surface,
title: _buildAppBarTitle(
context,
hasRail,
hasManage,
fullyExpanded,
),
leading: hasRail
? Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: IconButton(
icon: Icon(Symbols.menu, semanticLabel: navigationText),
tooltip: navigationText,
onPressed: fullyExpanded
? () {
ref
.read(_navigationProvider.notifier)
.toggleExpanded();
}
: () {
scaffoldGlobalKey.currentState?.openDrawer();
},
),
)),
const SizedBox(width: 12),
],
)
: Builder(
builder: (context) {
// Need to wrap with builder to get Scaffold context
return IconButton(
onPressed: () => Scaffold.of(context).openDrawer(),
icon: const Icon(Symbols.menu),
);
},
),
actions: [
if (widget.actionButtonBuilder == null &&
(widget.keyActionsBuilder != null && !hasManage))
Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
key: actionsIconButtonKey,
onPressed: () {
showBlurDialog(
context: context,
barrierColor: Colors.transparent,
builder: (context) => FsDialog(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: widget.keyActionsBuilder!(context),
scrolledUnderElevation: 0.0,
leadingWidth: hasRail ? 84 : null,
backgroundColor: Theme.of(context).colorScheme.surface,
title: _buildAppBarTitle(
context,
hasRail,
hasManage,
fullyExpanded,
),
centerTitle: true,
leading: hasRail
? Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: IconButton(
icon: Icon(Symbols.menu, semanticLabel: navigationText),
tooltip: navigationText,
onPressed: fullyExpanded
? () {
ref
.read(
_navigationVisibilityProvider.notifier)
.toggleExpanded();
}
: () {
scaffoldGlobalKey.currentState?.openDrawer();
},
),
),
);
},
icon: widget.keyActionsBadge
? Badge(
child: Icon(Symbols.more_vert,
semanticLabel: l10n.s_configure_yk),
)
: Icon(Symbols.more_vert,
semanticLabel: l10n.s_configure_yk),
iconSize: 24,
tooltip: l10n.s_configure_yk,
padding: const EdgeInsets.all(12),
)),
const SizedBox(width: 12),
],
)
: Builder(
builder: (context) {
// Need to wrap with builder to get Scaffold context
return IconButton(
onPressed: () => Scaffold.of(context).openDrawer(),
icon: const Icon(Symbols.menu),
);
},
),
actions: [
if (widget.actionButtonBuilder == null &&
(widget.keyActionsBuilder != null &&
(!hasManage || !showDetailView)))
Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
key: actionsIconButtonKey,
onPressed: () {
showBlurDialog(
context: context,
barrierColor: Colors.transparent,
builder: (context) => FsDialog(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: widget.keyActionsBuilder!(context),
),
),
);
},
icon: widget.keyActionsBadge
? Badge(
child: Icon(Symbols.more_vert,
semanticLabel: l10n.s_configure_yk),
)
: Icon(Symbols.more_vert,
semanticLabel: l10n.s_configure_yk),
iconSize: 24,
tooltip: l10n.s_configure_yk,
padding: const EdgeInsets.all(12),
),
),
),
if (widget.actionButtonBuilder != null)
Padding(
padding: const EdgeInsets.only(right: 12),
child: widget.actionButtonBuilder!.call(context),
),
],
if (hasManage &&
(widget.keyActionsBuilder != null ||
widget.detailViewBuilder != null))
Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
key: toggleDetailViewIconButtonKey,
onPressed: () {
ref
.read(_detailViewVisibilityProvider.notifier)
.toggleExpanded();
},
icon: const Icon(Symbols.view_sidebar),
iconSize: 24,
tooltip: showDetailView
? l10n.s_collapse_sidebar
: l10n.s_expand_sidebar,
padding: const EdgeInsets.all(12),
),
),
if (widget.actionButtonBuilder != null)
Padding(
padding: const EdgeInsets.only(right: 12),
child: widget.actionButtonBuilder!.call(context),
),
],
),
),
drawer: hasDrawer ? _buildDrawer(context) : null,
body: body,
@ -706,23 +748,66 @@ class _AppPageState extends ConsumerState<AppPage> {
}
}
class CapabilityBadge extends StatelessWidget {
final Capability capability;
class _GestureDetectorAppBar extends StatelessWidget
implements PreferredSizeWidget {
final AppBar appBar;
final void Function() onTap;
const CapabilityBadge(this.capability, {super.key});
const _GestureDetectorAppBar({required this.appBar, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.deferToChild, onTap: onTap, child: appBar);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class CapabilityBadge extends ConsumerWidget {
final Capability capability;
final bool noTooltip;
const CapabilityBadge(this.capability, {super.key, this.noTooltip = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final colorScheme = Theme.of(context).colorScheme;
final text = Text(capability.getDisplayName(l10n));
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(capability) ??
(false, false);
final label = fipsCapable
? Row(
children: [
Icon(
Symbols.shield,
color: colorScheme.onSecondaryContainer,
size: 12,
fill: fipsApproved ? 1 : 0,
),
const SizedBox(width: 4),
text,
],
)
: text;
return Badge(
backgroundColor: colorScheme.secondaryContainer,
textColor: colorScheme.onSecondaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 6),
largeSize: MediaQuery.of(context).textScaler.scale(20),
label: Text(
capability.getDisplayName(l10n),
),
label: fipsCapable && !noTooltip
? Tooltip(
message:
fipsApproved ? l10n.l_fips_approved : l10n.l_fips_capable,
child: label,
)
: label,
);
}
}

View File

@ -25,6 +25,8 @@ const _prefix = 'app.keys';
const deviceInfoListTile = Key('$_prefix.device_info_list_tile');
const noDeviceAvatar = Key('$_prefix.no_device_avatar');
const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
const toggleDetailViewIconButtonKey =
Key('$_prefix.toggle_detail_view_icon_button');
// drawer items
const homeDrawer = Key('$_prefix.drawer.home');

View File

@ -71,9 +71,9 @@ class MessagePage extends StatelessWidget {
delayedContent: delayedContent,
builder: (context, _) => Padding(
padding: EdgeInsets.only(
left: 16.0,
left: 18.0,
top: 0.0,
right: 16.0,
right: 18.0,
bottom: centered && actionsBuilder == null ? 96 : 0),
child: SizedBox(
width: centered ? 250 : 350,

View File

@ -163,6 +163,8 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
_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.

View File

@ -175,7 +175,7 @@ UserInteractionController _dialogUserInteraction(
builder: (context) {
return PopScope(
canPop: onCancel != null,
onPopInvoked: (didPop) {
onPopInvokedWithResult: (didPop, _) {
if (didPop) {
wasPopped = true;
if (!completed && onCancel != null) {

View File

@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../app/models.dart';
import '../widgets/flex_box.dart';
bool get isDesktop => const [
TargetPlatform.windows,
@ -119,3 +120,20 @@ final featureProvider = Provider<FeatureProvider>((ref) {
return isEnabled;
});
class LayoutNotifier extends StateNotifier<FlexLayout> {
final String _key;
final SharedPreferences _prefs;
LayoutNotifier(this._key, this._prefs)
: super(_fromName(_prefs.getString(_key)));
void setLayout(FlexLayout layout) {
state = layout;
_prefs.setString(_key, layout.name);
}
static FlexLayout _fromName(String? name) => FlexLayout.values.firstWhere(
(element) => element.name == name,
orElse: () => FlexLayout.list,
);
}

View File

@ -87,15 +87,16 @@ class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
return;
}
final pids = {
for (var e in (scan['pids'] as Map).entries)
UsbPid.fromValue(int.parse(e.key)): e.value as int
};
final numDevices = pids.values.fold<int>(0, (a, b) => a + b);
final numDevices =
(scan['pids'] as Map).values.fold<int>(0, (a, b) => a + b as int);
if (_usbState != scan['state'] || state.length != numDevices) {
var usbResult = await rpc.command('get', ['usb']);
_log.info('USB state change', jsonEncode(usbResult));
_usbState = usbResult['data']['state'];
final pids = {
for (var e in (usbResult['data']['pids'] as Map).entries)
UsbPid.fromValue(int.parse(e.key)): e.value as int
};
List<UsbYubiKeyNode> usbDevices = [];
for (String id in (usbResult['children'] as Map).keys) {
@ -224,11 +225,12 @@ final _desktopDeviceDataProvider =
ref.watch(rpcProvider).valueOrNull,
ref.watch(currentDeviceProvider),
);
if (notifier._deviceNode is NfcReaderNode) {
// If this is an NFC reader, listen on WindowState.
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
});
if (notifier._deviceNode is NfcReaderNode &&
ref.read(windowStateProvider).active) {
notifier._pollCard();
}
return notifier;
});
@ -243,6 +245,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
final RpcSession? _rpc;
final DeviceNode? _deviceNode;
Timer? _pollTimer;
StreamSubscription? _flagSubscription;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode)
: super(const AsyncValue.loading()) {
@ -255,11 +258,27 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
state = AsyncValue.error('device-inaccessible', StackTrace.current);
}
}
_flagSubscription = _rpc?.flags.listen(
(flag) {
if (flag == 'device_info') {
_pollDevice();
}
},
);
}
void _pollDevice() {
switch (_deviceNode) {
case UsbYubiKeyNode _:
_refreshUsb();
case NfcReaderNode _:
_pollCard();
}
}
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollCard();
_pollDevice();
} else {
_pollTimer?.cancel();
// TODO: Should we clear the key here?
@ -271,25 +290,39 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
@override
void dispose() {
_flagSubscription?.cancel();
_pollTimer?.cancel();
super.dispose();
}
void _refreshUsb() async {
final node = _deviceNode!;
var result = await _rpc?.command('get', node.path.segments);
if (mounted && result != null) {
final newState = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
if (state.valueOrNull != newState) {
_log.info('Configuration change in current USB device');
state = AsyncValue.data(newState);
}
}
}
void _pollCard() async {
_pollTimer?.cancel();
final node = _deviceNode!;
try {
_log.debug('Polling for NFC device changes...');
var result = await _rpc?.command('get', node.path.segments);
if (mounted && result != null) {
if (result['data']['present']) {
final oldState = state.valueOrNull;
final newState = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
if (oldState != null && oldState != newState) {
// Ensure state is cleared
state = const AsyncValue.loading();
} else {
if (oldState != newState) {
if (oldState != null) {
// Ensure state is cleared
state = const AsyncValue.loading();
}
state = AsyncValue.data(newState);
}
} else {

View File

@ -184,6 +184,12 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
rethrow;
}
}
@override
Future<void> enableEnterpriseAttestation() async {
await _session.command('enable_ep_attestation');
ref.invalidateSelf();
}
}
final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose

View File

@ -21,7 +21,8 @@ part 'models.g.dart';
@Freezed(unionKey: 'kind')
class RpcResponse with _$RpcResponse {
factory RpcResponse.success(Map<String, dynamic> body) = Success;
factory RpcResponse.success(Map<String, dynamic> body, List<String> flags) =
Success;
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
factory RpcResponse.error(
String status, String message, Map<String, dynamic> body) = RpcError;

View File

@ -34,7 +34,8 @@ mixin _$RpcResponse {
Map<String, dynamic> get body => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(Map<String, dynamic> body, List<String> flags)
success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
@ -43,7 +44,7 @@ mixin _$RpcResponse {
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(Map<String, dynamic> body)? success,
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
TResult? Function(String status, Map<String, dynamic> body)? signal,
TResult? Function(String status, String message, Map<String, dynamic> body)?
error,
@ -51,7 +52,7 @@ mixin _$RpcResponse {
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
@ -127,7 +128,7 @@ abstract class _$$SuccessImplCopyWith<$Res>
__$$SuccessImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({Map<String, dynamic> body});
$Res call({Map<String, dynamic> body, List<String> flags});
}
/// @nodoc
@ -142,12 +143,17 @@ class __$$SuccessImplCopyWithImpl<$Res>
@override
$Res call({
Object? body = null,
Object? flags = null,
}) {
return _then(_$SuccessImpl(
null == body
? _value._body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
null == flags
? _value._flags
: flags // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
@ -155,8 +161,10 @@ class __$$SuccessImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$SuccessImpl implements Success {
_$SuccessImpl(final Map<String, dynamic> body, {final String? $type})
_$SuccessImpl(final Map<String, dynamic> body, final List<String> flags,
{final String? $type})
: _body = body,
_flags = flags,
$type = $type ?? 'success';
factory _$SuccessImpl.fromJson(Map<String, dynamic> json) =>
@ -170,12 +178,20 @@ class _$SuccessImpl implements Success {
return EqualUnmodifiableMapView(_body);
}
final List<String> _flags;
@override
List<String> get flags {
if (_flags is EqualUnmodifiableListView) return _flags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_flags);
}
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.success(body: $body)';
return 'RpcResponse.success(body: $body, flags: $flags)';
}
@override
@ -183,13 +199,16 @@ class _$SuccessImpl implements Success {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuccessImpl &&
const DeepCollectionEquality().equals(other._body, _body));
const DeepCollectionEquality().equals(other._body, _body) &&
const DeepCollectionEquality().equals(other._flags, _flags));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_body));
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_body),
const DeepCollectionEquality().hash(_flags));
@JsonKey(ignore: true)
@override
@ -200,37 +219,38 @@ class _$SuccessImpl implements Success {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(Map<String, dynamic> body, List<String> flags)
success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return success(body);
return success(body, flags);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(Map<String, dynamic> body)? success,
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
TResult? Function(String status, Map<String, dynamic> body)? signal,
TResult? Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return success?.call(body);
return success?.call(body, flags);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (success != null) {
return success(body);
return success(body, flags);
}
return orElse();
}
@ -278,12 +298,14 @@ class _$SuccessImpl implements Success {
}
abstract class Success implements RpcResponse {
factory Success(final Map<String, dynamic> body) = _$SuccessImpl;
factory Success(final Map<String, dynamic> body, final List<String> flags) =
_$SuccessImpl;
factory Success.fromJson(Map<String, dynamic> json) = _$SuccessImpl.fromJson;
@override
Map<String, dynamic> get body;
List<String> get flags;
@override
@JsonKey(ignore: true)
_$$SuccessImplCopyWith<_$SuccessImpl> get copyWith =>
@ -380,7 +402,8 @@ class _$SignalImpl implements Signal {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(Map<String, dynamic> body, List<String> flags)
success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
@ -392,7 +415,7 @@ class _$SignalImpl implements Signal {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(Map<String, dynamic> body)? success,
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
TResult? Function(String status, Map<String, dynamic> body)? signal,
TResult? Function(String status, String message, Map<String, dynamic> body)?
error,
@ -403,7 +426,7 @@ class _$SignalImpl implements Signal {
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
@ -570,7 +593,8 @@ class _$RpcErrorImpl implements RpcError {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(Map<String, dynamic> body, List<String> flags)
success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
@ -582,7 +606,7 @@ class _$RpcErrorImpl implements RpcError {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(Map<String, dynamic> body)? success,
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
TResult? Function(String status, Map<String, dynamic> body)? signal,
TResult? Function(String status, String message, Map<String, dynamic> body)?
error,
@ -593,7 +617,7 @@ class _$RpcErrorImpl implements RpcError {
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,

View File

@ -9,12 +9,14 @@ part of 'models.dart';
_$SuccessImpl _$$SuccessImplFromJson(Map<String, dynamic> json) =>
_$SuccessImpl(
json['body'] as Map<String, dynamic>,
(json['flags'] as List<dynamic>).map((e) => e as String).toList(),
$type: json['kind'] as String?,
);
Map<String, dynamic> _$$SuccessImplToJson(_$SuccessImpl instance) =>
<String, dynamic>{
'body': instance.body,
'flags': instance.flags,
'kind': instance.$type,
};

View File

@ -102,8 +102,12 @@ class RpcSession {
final String executable;
late _RpcConnection _connection;
final StreamController<_Request> _requests = StreamController();
final StreamController<String> _flags = StreamController();
late final Stream<String> flags;
RpcSession(this.executable);
RpcSession(this.executable) {
flags = _flags.stream.asBroadcastStream();
}
static void _logEntry(String entry) {
try {
@ -230,7 +234,7 @@ class RpcSession {
Future<Map<String, dynamic>> command(String action, List<String>? target,
{Map? params, Signaler? signal}) {
var request = _Request(action, target ?? [], params ?? {}, signal);
final request = _Request(action, target ?? [], params ?? {}, signal);
_requests.add(request);
return request.completer.future;
}
@ -278,6 +282,10 @@ class RpcSession {
},
success: (success) {
request.completer.complete(success.body);
for (final flag in success.flags) {
_log.traffic('FLAG', flag);
_flags.add(flag);
}
completed = true;
},
error: (error) {

View File

@ -21,6 +21,8 @@ final actions = fido.feature('actions');
final actionsPin = actions.feature('pin');
final actionsAddFingerprint = actions.feature('addFingerprint');
final actionsReset = actions.feature('reset');
final enableEnterpriseAttestation =
actions.feature('enableEnterpriseAttestation');
final credentials = fido.feature('credentials');

View File

@ -25,6 +25,8 @@ const _credentialInfo = '$_prefix.credential.info';
// Key actions
const managePinAction = Key('$_keyAction.manage_pin');
const addFingerprintAction = Key('$_keyAction.add_fingerprint');
const enableEnterpriseAttestation =
Key('$_keyAction.enable_enterprise_attestation');
const newPin = Key('$_keyAction.new_pin');
const confirmPin = Key('$_keyAction.confirm_pin');
const currentPin = Key('$_keyAction.current_pin');

View File

@ -50,6 +50,8 @@ class FidoState with _$FidoState {
bool get forcePinChange => info['force_pin_change'] == true;
bool get pinBlocked => pinRetries == 0;
bool? get enterpriseAttestation => info['options']['ep'];
}
@freezed

View File

@ -10,7 +10,7 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map<String, dynamic> json) =>
_$FidoStateImpl(
info: json['info'] as Map<String, dynamic>,
unlocked: json['unlocked'] as bool,
pinRetries: json['pin_retries'] as int?,
pinRetries: (json['pin_retries'] as num?)?.toInt(),
);
Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) =>

View File

@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/models.dart';
import '../core/state.dart';
import '../widgets/flex_box.dart';
import 'models.dart';
final passkeysSearchProvider =
@ -32,6 +33,11 @@ class PasskeysSearchNotifier extends StateNotifier<String> {
}
}
final passkeysLayoutProvider =
StateNotifierProvider<LayoutNotifier, FlexLayout>(
(ref) => LayoutNotifier('FIDO_PASSKEYS_LAYOUT', ref.watch(prefProvider)),
);
final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
() => throw UnimplementedError(),
@ -41,6 +47,7 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
Stream<InteractionEvent> reset();
Future<PinResult> setPin(String newPin, {String? oldPin});
Future<PinResult> unlock(String pin);
Future<void> enableEnterpriseAttestation();
}
final fingerprintProvider = AsyncNotifierProvider.autoDispose

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2023 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.
@ -123,6 +123,8 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
});
}, onError: (error, stacktrace) {
_log.error('Error adding fingerprint', error, stacktrace);
if (!mounted) return;
Navigator.of(context).pop();
final l10n = AppLocalizations.of(context)!;
final String errorMessage;
@ -255,7 +257,11 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
});
},
onFieldSubmitted: (_) {
_submit();
if (_label.isNotEmpty) {
_submit();
} else {
_nameFocus.requestFocus();
}
},
).init(),
)

View File

@ -0,0 +1,50 @@
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 EnableEnterpriseAttestationDialog extends ConsumerWidget {
final DevicePath devicePath;
const EnableEnterpriseAttestationDialog(this.devicePath, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return ResponsiveDialog(
title: Text(l10n.s_enable_ep_attestation),
actions: [
TextButton(
onPressed: () async {
await ref
.read(fidoStateProvider(devicePath).notifier)
.enableEnterpriseAttestation();
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
showMessage(context, l10n.s_ep_attestation_enabled);
});
},
child: Text(l10n.s_enable),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_enable_ep_attestation_desc),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
),
),
);
}
}

View File

@ -331,15 +331,18 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
}),
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: fingerprints
.map((fp) => _FingerprintListItem(
fp,
expanded: expanded,
selected: fp == _selected,
))
.toList()),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: fingerprints
.map((fp) => _FingerprintListItem(
fp,
expanded: expanded,
selected: fp == _selected,
))
.toList()),
),
);
},
),

View File

@ -25,6 +25,7 @@ import '../features.dart' as features;
import '../keys.dart' as keys;
import '../models.dart';
import 'add_fingerprint_dialog.dart';
import 'enterprise_attestation_dialog.dart';
import 'pin_dialog.dart';
bool passkeysShowActionsNotifier(FidoState state) {
@ -50,6 +51,13 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
Theme.of(context).colorScheme;
final authBlocked = state.pinBlocked;
final enterpriseAttestation = state.enterpriseAttestation;
final showEnterpriseAttestation = enterpriseAttestation != null &&
!(state.alwaysUv && !state.hasPin) &&
!(!state.unlocked && state.hasPin);
final canEnableEnterpriseAttestation =
enterpriseAttestation == false && showEnterpriseAttestation;
return Column(
children: [
if (fingerprints != null)
@ -115,8 +123,27 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
}
: null,
),
if (showEnterpriseAttestation)
ActionListItem(
key: keys.enableEnterpriseAttestation,
feature: features.enableEnterpriseAttestation,
icon: const Icon(Symbols.local_police),
title: l10n.s_ep_attestation,
subtitle:
enterpriseAttestation ? l10n.s_enabled : l10n.s_disabled,
onTap: canEnableEnterpriseAttestation
? (context) {
Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) =>
EnableEnterpriseAttestationDialog(node.path),
);
}
: null,
)
],
)
),
],
);
}

View File

@ -38,6 +38,7 @@ import '../../exception/no_data_exception.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_form_field.dart';
import '../../widgets/flex_box.dart';
import '../../widgets/list_title.dart';
import '../features.dart' as features;
import '../models.dart';
@ -219,6 +220,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
late FocusNode searchFocus;
late TextEditingController searchController;
FidoCredential? _selected;
bool _canRequestFocus = true;
@override
void initState() {
@ -374,60 +376,103 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
}
return KeyEventResult.ignored;
},
child: Builder(builder: (context) {
child: LayoutBuilder(builder: (context, constraints) {
final textTheme = Theme.of(context).textTheme;
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: AppTextFormField(
key: searchField,
controller: searchController,
focusNode: searchFocus,
// Use the default style, but with a smaller font size:
style: textTheme.titleMedium
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
decoration: AppInputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(48),
borderSide: BorderSide(
width: 0,
style: searchFocus.hasFocus
? BorderStyle.solid
: BorderStyle.none,
final width = constraints.maxWidth;
final showLayoutOptions = width > 600;
return Consumer(
builder: (context, ref, child) {
final layout = ref.watch(passkeysLayoutProvider);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 8.0),
child: AppTextFormField(
key: searchField,
controller: searchController,
canRequestFocus: _canRequestFocus,
focusNode: searchFocus,
// Use the default style, but with a smaller font size:
style: textTheme.titleMedium
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
decoration: AppInputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(48),
borderSide: BorderSide(
width: 0,
style: searchFocus.hasFocus
? BorderStyle.solid
: BorderStyle.none,
),
),
contentPadding: const EdgeInsets.all(16),
fillColor: Theme.of(context).hoverColor,
filled: true,
hintText: l10n.s_search_passkeys,
isDense: true,
prefixIcon: const Padding(
padding: EdgeInsetsDirectional.only(start: 8.0),
child: Icon(Icons.search_outlined),
),
suffixIcons: [
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
iconSize: 16,
onPressed: () {
searchController.clear();
ref
.read(passkeysSearchProvider.notifier)
.setFilter('');
setState(() {});
},
),
if (searchController.text.isEmpty && showLayoutOptions)
...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,
),
),
),
),
],
),
),
contentPadding: const EdgeInsets.all(16),
fillColor: Theme.of(context).hoverColor,
filled: true,
hintText: l10n.s_search_passkeys,
isDense: true,
prefixIcon: const Padding(
padding: EdgeInsetsDirectional.only(start: 8.0),
child: Icon(Icons.search_outlined),
),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
iconSize: 16,
onPressed: () {
searchController.clear();
ref
.read(passkeysSearchProvider.notifier)
.setFilter('');
setState(() {});
},
)
: null,
),
onChanged: (value) {
ref.read(passkeysSearchProvider.notifier).setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context).focusInDirection(TraversalDirection.down);
},
).init(),
onChanged: (value) {
ref
.read(passkeysSearchProvider.notifier)
.setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context)
.focusInDirection(TraversalDirection.down);
},
).init(),
);
},
);
}),
),
@ -483,21 +528,37 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
}),
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (filteredCredentials.isEmpty)
Center(
child: Text(l10n.s_no_passkeys),
child: Consumer(
builder: (context, ref, child) {
final layout = ref.watch(passkeysLayoutProvider);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (filteredCredentials.isEmpty)
Center(
child: Text(l10n.s_no_passkeys),
),
FlexBox<FidoCredential>(
items: filteredCredentials,
itemBuilder: (cred) => _CredentialListItem(
cred,
expanded: expanded,
selected: _selected == cred,
tileColor: layout == FlexLayout.grid
? Theme.of(context).hoverColor
: null,
),
layout: layout,
cellMinWidth: 265,
spacing: layout == FlexLayout.grid ? 4.0 : 0.0,
runSpacing: layout == FlexLayout.grid ? 4.0 : 0.0,
)
],
),
...filteredCredentials.map(
(cred) => _CredentialListItem(
cred,
expanded: expanded,
selected: _selected == cred,
),
),
],
);
},
),
);
},
@ -518,9 +579,10 @@ class _CredentialListItem extends StatelessWidget {
final FidoCredential credential;
final bool selected;
final bool expanded;
final Color? tileColor;
const _CredentialListItem(this.credential,
{required this.expanded, required this.selected});
{required this.expanded, required this.selected, this.tileColor});
@override
Widget build(BuildContext context) {
@ -533,6 +595,7 @@ class _CredentialListItem extends StatelessWidget {
backgroundColor: colorScheme.secondary,
child: const Icon(Symbols.passkey),
),
tileColor: tileColor,
title: credential.rpId,
subtitle: credential.userName,
trailing: expanded

View File

@ -52,6 +52,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
final _currentPinFocus = FocusNode();
final _newPinController = TextEditingController();
final _newPinFocus = FocusNode();
final _confirmPinFocus = FocusNode();
String _confirmPin = '';
String? _currentPinError;
String? _newPinError;
@ -68,6 +69,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_currentPinFocus.dispose();
_newPinController.dispose();
_newPinFocus.dispose();
_confirmPinFocus.dispose();
super.dispose();
}
@ -86,6 +88,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
final isValid =
currentPinLenOk && newPinLenOk && _newPinController.text == _confirmPin;
final newPinEnabled = !_isBlocked && currentPinLenOk;
final confirmPinEnabled = !_isBlocked && currentPinLenOk && newPinLenOk;
final deviceData = ref.read(currentDeviceDataProvider).valueOrNull;
final hasPinComplexity = deviceData?.info.pinComplexity ?? false;
@ -148,11 +153,19 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin,
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
});
},
onFieldSubmitted: (_) {
if (_currentPinController.text.length < minPinLength) {
_currentPinFocus.requestFocus();
} else {
_newPinFocus.requestFocus();
}
},
).init(),
],
Text(hasPinComplexity
@ -172,31 +185,43 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_new_pin,
enabled: !_isBlocked && currentPinLenOk,
enabled: newPinEnabled,
errorText: _newIsWrong ? _newPinError : null,
errorMaxLines: 3,
prefixIcon: const Icon(Symbols.pin),
suffixIcon: IconButton(
icon: Icon(_isObscureNew
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureNew = !_isObscureNew;
});
},
tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin,
suffixIcon: ExcludeFocusTraversal(
excluding: !newPinEnabled,
child: IconButton(
icon: Icon(_isObscureNew
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureNew = !_isObscureNew;
});
},
tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin,
),
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_newIsWrong = false;
});
},
onFieldSubmitted: (_) {
if (_newPinController.text.length < minPinLength) {
_newPinFocus.requestFocus();
} else {
_confirmPinFocus.requestFocus();
}
},
).init(),
AppTextFormField(
key: confirmPin,
initialValue: _confirmPin,
focusNode: _confirmPinFocus,
maxLength: maxPinLength,
inputFormatters: [limitBytesLength(maxPinLength)],
buildCounter: buildByteCounterFor(_confirmPin),
@ -206,19 +231,22 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
border: const OutlineInputBorder(),
labelText: l10n.s_confirm_pin,
prefixIcon: const Icon(Symbols.pin),
suffixIcon: IconButton(
icon: Icon(_isObscureConfirm
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureConfirm = !_isObscureConfirm;
});
},
tooltip:
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
suffixIcon: ExcludeFocusTraversal(
excluding: !confirmPinEnabled,
child: IconButton(
icon: Icon(_isObscureConfirm
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureConfirm = !_isObscureConfirm;
});
},
tooltip:
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
),
),
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
enabled: confirmPinEnabled,
errorText:
_newPinController.text.length == _confirmPin.length &&
_newPinController.text != _confirmPin
@ -226,6 +254,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
: null,
helperText: '', // Prevents resizing when errorText shown
),
textInputAction: TextInputAction.done,
onChanged: (value) {
setState(() {
_confirmPin = value;
@ -234,6 +263,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
onFieldSubmitted: (_) {
if (isValid) {
_submit();
} else {
_confirmPinFocus.requestFocus();
}
},
).init(),

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2023 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.
@ -41,12 +41,20 @@ class RenameFingerprintDialog extends ConsumerStatefulWidget {
class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
late String _label;
late FocusNode _labelFocus;
_RenameAccountDialogState();
@override
void initState() {
super.initState();
_label = widget.fingerprint.name ?? '';
_labelFocus = FocusNode();
}
@override
void dispose() {
_labelFocus.dispose();
super.dispose();
}
_submit() async {
@ -93,7 +101,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
Text(l10n.q_rename_target(widget.fingerprint.label)),
Text(l10n.p_will_change_label_fp),
AppTextFormField(
autofocus: true,
initialValue: _label,
focusNode: _labelFocus,
maxLength: 15,
inputFormatters: [limitBytesLength(15)],
buildCounter: buildByteCounterFor(_label),
@ -110,6 +120,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
onFieldSubmitted: (_) {
if (_label.isNotEmpty) {
_submit();
} else {
_labelFocus.requestFocus();
}
},
).init(),

View File

@ -29,7 +29,6 @@ import '../../app/views/app_page.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/product_image.dart';
import 'key_actions.dart';
import 'manage_label_dialog.dart';
@ -72,7 +71,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
homeBuildActions(context, widget.deviceData, ref),
builder: (context, expanded) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -92,16 +91,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
runSpacing: 8,
children: Capability.values
.where((c) => enabledCapabilities & c.value != 0)
.map((c) => CapabilityBadge(c))
.map((c) => CapabilityBadge(c, noTooltip: true))
.toList(),
),
if (serial != null) ...[
const SizedBox(height: 32.0),
_DeviceColor(
deviceData: widget.deviceData,
initialCustomization: keyCustomization ??
KeyCustomization(serial: serial))
]
if (widget.deviceData.info.fipsCapable != 0)
Padding(
padding: const EdgeInsets.only(top: 38),
child: _FipsLegend(),
),
],
),
),
@ -131,6 +128,62 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
}
}
class _FipsLegend extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Opacity(
opacity: 0.6,
child: Wrap(
runSpacing: 0,
spacing: 16,
children: [
RichText(
text: TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 0.0,
),
),
),
TextSpan(
text: l10n.l_fips_capable,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
RichText(
text: TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 1.0,
),
),
),
TextSpan(
text: l10n.l_fips_approved,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
],
),
);
}
}
class _DeviceContent extends ConsumerWidget {
final YubiKeyData deviceData;
final KeyCustomization? initialCustomization;
@ -147,6 +200,9 @@ class _DeviceContent extends ConsumerWidget {
final label = initialCustomization?.name;
String displayName = label != null ? '$label ($name)' : name;
final defaultColor = ref.watch(defaultColorProvider);
final customColor = initialCustomization?.color;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -158,22 +214,114 @@ class _DeviceContent extends ConsumerWidget {
style: Theme.of(context).textTheme.titleLarge,
),
),
if (serial != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: IconButton(
icon: const Icon(Symbols.edit),
onPressed: () async {
await ref.read(withContextProvider)((context) async {
await _showManageLabelDialog(
initialCustomization ??
KeyCustomization(serial: serial),
context,
);
});
},
),
if (serial != null) ...[
const SizedBox(width: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Symbols.edit),
tooltip: l10n.s_set_label,
color: Theme.of(context).colorScheme.onSurfaceVariant,
onPressed: () async {
await ref.read(withContextProvider)((context) async {
await _showManageLabelDialog(
initialCustomization ??
KeyCustomization(serial: serial),
context,
);
});
},
),
Column(
children: [
PopupMenuButton(
popUpAnimationStyle:
AnimationStyle(duration: Duration.zero),
menuPadding: EdgeInsets.zero,
tooltip: l10n.s_set_color,
itemBuilder: (context) {
return [
PopupMenuItem(
child: Center(
child: Wrap(
runSpacing: 8,
spacing: 16,
children: [
...[
Colors.teal,
Colors.cyan,
Colors.blueAccent,
Colors.deepPurple,
Colors.red,
Colors.orange,
Colors.yellow,
// add nice color to devices with dynamic color
if (isAndroid &&
ref.read(androidSdkVersionProvider) >=
31)
Colors.lightGreen
].map((e) => _ColorButton(
color: e,
isSelected:
customColor?.value == e.value,
onPressed: () {
_updateColor(e, ref, serial);
Navigator.of(context).pop();
},
)),
// "use default color" button
RawMaterialButton(
onPressed: () {
_updateColor(null, ref, serial);
Navigator.of(context).pop();
},
constraints: const BoxConstraints(
minWidth: 26.0, minHeight: 26.0),
fillColor: defaultColor,
hoverColor: Colors.black12,
shape: const CircleBorder(),
child: Icon(
customColor == null
? Symbols.circle
: Symbols.clear,
fill: 1,
size: 16,
weight: 700,
opticalSize: 20,
color: defaultColor
.computeLuminance() >
0.7
? Colors.grey // for bright colors
: Colors.white),
),
],
),
),
)
];
},
icon: Icon(
Symbols.palette,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Container(
height: 3.0,
width: 24.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.9)),
)
],
),
],
)
]
],
),
const SizedBox(
@ -195,10 +343,42 @@ class _DeviceContent extends ConsumerWidget {
.titleSmall
?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (deviceData.info.pinComplexity)
Padding(
padding: const EdgeInsets.only(top: 12),
child: RichText(
text: TextSpan(children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(
Symbols.check,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
)),
TextSpan(
text: l10n.l_pin_complexity,
style: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
]),
),
),
],
);
}
void _updateColor(Color? color, WidgetRef ref, int serial) async {
final manager = ref.read(keyCustomizationManagerProvider.notifier);
await manager.set(
serial: serial,
name: initialCustomization?.name,
color: color,
);
}
Future<void> _showManageLabelDialog(
KeyCustomization keyCustomization, BuildContext context) async {
await showBlurDialog(
@ -210,97 +390,6 @@ class _DeviceContent extends ConsumerWidget {
}
}
class _DeviceColor extends ConsumerWidget {
final YubiKeyData deviceData;
final KeyCustomization initialCustomization;
const _DeviceColor(
{required this.deviceData, required this.initialCustomization});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final defaultColor = ref.watch(defaultColorProvider);
final customColor = initialCustomization.color;
return ChoiceFilterChip<Color?>(
disableHover: true,
value: customColor,
items: const [null],
selected: customColor != null && customColor.value != defaultColor.value,
itemBuilder: (e) => Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
spacing: 16,
children: [
...[
Colors.teal,
Colors.cyan,
Colors.blueAccent,
Colors.deepPurple,
Colors.red,
Colors.orange,
Colors.yellow,
// add nice color to devices with dynamic color
if (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
Colors.lightGreen
].map((e) => _ColorButton(
color: e,
isSelected: customColor?.value == e.value,
onPressed: () {
_updateColor(e, ref);
Navigator.of(context).pop();
},
)),
// "use default color" button
RawMaterialButton(
onPressed: () {
_updateColor(null, ref);
Navigator.of(context).pop();
},
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
fillColor: defaultColor,
hoverColor: Colors.black12,
shape: const CircleBorder(),
child: Icon(customColor == null ? Symbols.circle : Symbols.clear,
fill: 1,
size: 16,
weight: 700,
opticalSize: 20,
color: defaultColor.computeLuminance() > 0.7
? Colors.grey // for bright colors
: Colors.white),
),
],
),
labelBuilder: (e) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: const BoxConstraints(minWidth: 22.0, minHeight: 22.0),
decoration: BoxDecoration(
color: customColor ?? defaultColor, shape: BoxShape.circle),
),
const SizedBox(
width: 12,
),
Flexible(child: Text(l10n.s_color))
],
),
onChanged: (e) {},
);
}
void _updateColor(Color? color, WidgetRef ref) async {
final manager = ref.read(keyCustomizationManagerProvider.notifier);
await manager.set(
serial: initialCustomization.serial,
name: initialCustomization.name,
color: color,
);
}
}
class _ColorButton extends StatefulWidget {
final Color? color;
final bool isSelected;

View File

@ -34,6 +34,7 @@ Widget homeBuildActions(
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider);
final interfacesLocked = deviceData?.info.resetBlocked != 0;
final managementAvailability = hasFeature(features.management) &&
switch (deviceData?.info.version) {
Version version => (version.major > 4 || // YK5 and up
@ -56,16 +57,21 @@ Widget homeBuildActions(
title: deviceData.info.version.major > 4
? l10n.s_toggle_applications
: l10n.s_toggle_interfaces,
subtitle: deviceData.info.version.major > 4
? l10n.l_toggle_applications_desc
: l10n.l_toggle_interfaces_desc,
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) => ManagementScreen(deviceData),
);
},
subtitle: interfacesLocked
? 'Requires factory reset' // TODO: Replace with l10n
: (deviceData.info.version.major > 4
? l10n.l_toggle_applications_desc
: l10n.l_toggle_interfaces_desc),
onTap: interfacesLocked
? null
: (context) {
Navigator.of(context)
.popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) => ManagementScreen(deviceData),
);
},
),
if (getResetCapabilities(hasFeature).any((c) =>
c.value &

View File

@ -30,6 +30,9 @@
"s_delete": "Löschen",
"s_move": null,
"s_quit": "Beenden",
"s_enable": null,
"s_enabled": null,
"s_disabled": null,
"s_status": null,
"s_unlock": "Entsperren",
"s_calculate": "Berechnen",
@ -45,6 +48,8 @@
"s_hide_window": "Fenster verstecken",
"s_expand_navigation": null,
"s_collapse_navigation": null,
"s_expand_sidebar": null,
"s_collapse_sidebar": null,
"q_rename_target": "{label} umbenennen?",
"@q_rename_target": {
"placeholders": {
@ -121,6 +126,12 @@
"s_light_mode": "Heller Modus",
"s_dark_mode": "Dunkler Modus",
"@_layout": {},
"s_list_layout": null,
"s_grid_layout": null,
"s_mixed_layout": null,
"s_select_layout": null,
"@_yubikey_selection": {},
"s_select_to_scan": "Zum Scannen auswählen",
"s_hide_device": "Gerät verstecken",
@ -149,6 +160,8 @@
}
},
"l_firmware_version": null,
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKey anschließen",
@ -301,6 +314,10 @@
"common_pin": {}
}
},
"s_ep_attestation": null,
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": null,
"s_pin_required": null,
"p_pin_required_desc": null,
"l_piv_pin_blocked": null,
@ -330,6 +347,7 @@
"l_warning_default_puk": null,
"l_default_pin_used": null,
"l_default_puk_used": null,
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Passwort",
@ -339,6 +357,7 @@
"s_show_password": null,
"s_hide_password": null,
"l_optional_password_protection": "Optionaler Passwortschutz",
"l_password_protection": null,
"s_new_password": "Neues Passwort",
"s_current_password": "Aktuelles Passwort",
"s_confirm_password": "Passwort bestätigen",
@ -352,6 +371,7 @@
"l_keystore_unavailable": "Passwortspeicher des Betriebssystems nicht verfügbar",
"l_remember_pw_failed": "Konnte Passwort nicht speichern",
"l_unlock_first": "Zuerst mit Passwort entsperren",
"l_set_password_first": null,
"l_enter_oath_pw": "Das OATH-Passwort für Ihren YubiKey eingeben",
"p_enter_current_password_or_reset": "Geben Sie Ihr aktuelles Passwort ein. Wenn Sie Ihr Passwort nicht wissen, müssen Sie den YubiKey zurücksetzen.",
"p_enter_new_password": "Geben Sie Ihr neues Passwort ein. Ein Passwort kann Buchstaben, Ziffern und spezielle Zeichen enthalten.",
@ -410,6 +430,7 @@
"s_pin_account": "Konto anpinnen",
"s_unpin_account": "Konto nicht mehr anpinnen",
"s_no_pinned_accounts": "Keine angepinnten Konten",
"s_pinned": null,
"l_pin_account_desc": null,
"s_rename_account": "Konto umbenennen",
"l_rename_account_desc": null,
@ -885,6 +906,7 @@
"@_key_customization": {},
"s_set_label": null,
"s_set_color": null,
"s_change_label": null,
"s_color": null,
"p_set_will_add_custom_name": null,

View File

@ -30,6 +30,9 @@
"s_delete": "Delete",
"s_move": "Move",
"s_quit": "Quit",
"s_enable": "Enable",
"s_enabled": "Enabled",
"s_disabled": "Disabled",
"s_status": "Status",
"s_unlock": "Unlock",
"s_calculate": "Calculate",
@ -45,6 +48,8 @@
"s_hide_window": "Hide window",
"s_expand_navigation": "Expand navigation",
"s_collapse_navigation": "Collapse navigation",
"s_expand_sidebar": "Expand sidebar",
"s_collapse_sidebar": "Collapse sidebar",
"q_rename_target": "Rename {label}?",
"@q_rename_target": {
"placeholders": {
@ -121,6 +126,12 @@
"s_light_mode": "Light mode",
"s_dark_mode": "Dark mode",
"@_layout": {},
"s_list_layout": "List layout",
"s_grid_layout": "Grid layout",
"s_mixed_layout": "Mixed layout",
"s_select_layout": "Select layout",
"@_yubikey_selection": {},
"s_select_to_scan": "Select to scan",
"s_hide_device": "Hide device",
@ -149,6 +160,8 @@
}
},
"l_firmware_version": "Firmware version: {version}",
"l_fips_capable": "FIPS capable",
"l_fips_approved": "FIPS approved",
"@_yubikey_interactions": {},
"l_insert_yk": "Insert your YubiKey",
@ -301,6 +314,10 @@
"common_pin": {}
}
},
"s_ep_attestation": "Enterprise Attestation",
"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.",
"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",
@ -330,6 +347,7 @@
"l_warning_default_puk": "Warning: Default PUK used",
"l_default_pin_used": "Default PIN used",
"l_default_puk_used": "Default PUK used",
"l_pin_complexity": "PIN complexity enforced",
"@_passwords": {},
"s_password": "Password",
@ -339,6 +357,7 @@
"s_show_password": "Show password",
"s_hide_password": "Hide password",
"l_optional_password_protection": "Optional password protection",
"l_password_protection": "Password protection of accounts",
"s_new_password": "New password",
"s_current_password": "Current password",
"s_confirm_password": "Confirm password",
@ -352,6 +371,7 @@
"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_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.",
@ -410,6 +430,7 @@
"s_pin_account": "Pin account",
"s_unpin_account": "Unpin account",
"s_no_pinned_accounts": "No pinned accounts",
"s_pinned": "Pinned",
"l_pin_account_desc": "Keep your important accounts together",
"s_rename_account": "Rename account",
"l_rename_account_desc": "Edit the issuer/name of the account",
@ -885,6 +906,7 @@
"@_key_customization": {},
"s_set_label": "Set label",
"s_set_color": "Set color",
"s_change_label": "Change label",
"s_color": "Color",
"p_set_will_add_custom_name": "This will give your YubiKey a custom name.",

View File

@ -30,6 +30,9 @@
"s_delete": "Supprimer",
"s_move": "Déplacer",
"s_quit": "Quitter",
"s_enable": null,
"s_enabled": null,
"s_disabled": null,
"s_status": "État",
"s_unlock": "Déverrouiller",
"s_calculate": "Calculer",
@ -45,6 +48,8 @@
"s_hide_window": "Masquer fenêtre",
"s_expand_navigation": "Développer la navigation",
"s_collapse_navigation": "Réduire la navigation",
"s_expand_sidebar": null,
"s_collapse_sidebar": null,
"q_rename_target": "Renommer {label}\u00a0?",
"@q_rename_target": {
"placeholders": {
@ -121,6 +126,12 @@
"s_light_mode": "Thème clair",
"s_dark_mode": "Thème sombre",
"@_layout": {},
"s_list_layout": null,
"s_grid_layout": null,
"s_mixed_layout": null,
"s_select_layout": null,
"@_yubikey_selection": {},
"s_select_to_scan": "Sélectionner pour scanner",
"s_hide_device": "Masquer appareil",
@ -149,6 +160,8 @@
}
},
"l_firmware_version": "Version du firmware : {version}",
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "Insérez votre YubiKey",
@ -301,6 +314,10 @@
"common_pin": {}
}
},
"s_ep_attestation": null,
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": 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",
@ -330,6 +347,7 @@
"l_warning_default_puk": "Attention : PUK par défaut utilisé",
"l_default_pin_used": "Code PIN par défaut utilisé",
"l_default_puk_used": "PUK par défaut utilisé",
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Mot de passe",
@ -339,6 +357,7 @@
"s_show_password": "Montrer mot de passe",
"s_hide_password": "Masquer mot de passe",
"l_optional_password_protection": "Protection par mot de passe facultative",
"l_password_protection": null,
"s_new_password": "Nouveau mot de passe",
"s_current_password": "Mot de passe actuel",
"s_confirm_password": "Confirmer mot de passe",
@ -352,6 +371,7 @@
"l_keystore_unavailable": "OS Keystore indisponible",
"l_remember_pw_failed": "Mémorisation mot de passe impossible",
"l_unlock_first": "Débloquez d'abord avec mot de passe",
"l_set_password_first": null,
"l_enter_oath_pw": "Saisissez le mot de passe OATH de votre YubiKey",
"p_enter_current_password_or_reset": "Saisissez votre mot de passe actuel. Vous ne connaissez votre mot de passe\u00a0? Réinitialisez la YubiKey.",
"p_enter_new_password": "Saisissez votre nouveau mot de passe. Un mot de passe peut inclure des lettres, chiffres et caractères spéciaux.",
@ -410,6 +430,7 @@
"s_pin_account": "Épingler compte",
"s_unpin_account": "Détacher compte",
"s_no_pinned_accounts": "Aucun compte épinglé",
"s_pinned": null,
"l_pin_account_desc": "Conserver vos comptes importants ensemble",
"s_rename_account": "Renommer compte",
"l_rename_account_desc": "Modifier émetteur/nom du compte",
@ -885,6 +906,7 @@
"@_key_customization": {},
"s_set_label": "Définir l'étiquette",
"s_set_color": null,
"s_change_label": "Modifier l'étiquette",
"s_color": "Couleur",
"p_set_will_add_custom_name": "Cela donnera un nom personnalisé à votre YubiKey.",

View File

@ -30,6 +30,9 @@
"s_delete": "削除",
"s_move": "移動",
"s_quit": "終了",
"s_enable": null,
"s_enabled": null,
"s_disabled": null,
"s_status": "ステータス",
"s_unlock": "ロック解除",
"s_calculate": "計算",
@ -45,6 +48,8 @@
"s_hide_window": "ウィンドウを非表示",
"s_expand_navigation": "ナビゲーションを展開",
"s_collapse_navigation": "ナビゲーションを閉じる",
"s_expand_sidebar": null,
"s_collapse_sidebar": null,
"q_rename_target": "{label}の名前を変更しますか?",
"@q_rename_target": {
"placeholders": {
@ -121,6 +126,12 @@
"s_light_mode": "ライトモード",
"s_dark_mode": "ダークモード",
"@_layout": {},
"s_list_layout": null,
"s_grid_layout": null,
"s_mixed_layout": null,
"s_select_layout": null,
"@_yubikey_selection": {},
"s_select_to_scan": "選択してスキャン",
"s_hide_device": "デバイスを非表示",
@ -149,6 +160,8 @@
}
},
"l_firmware_version": "ファームウェアバージョン: {version}",
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKeyを挿入してください",
@ -301,6 +314,10 @@
"common_pin": {}
}
},
"s_ep_attestation": null,
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": null,
"s_pin_required": "PINが必要",
"p_pin_required_desc": "実行しようとしているアクションでは、PIV PINを入力する必要があります。",
"l_piv_pin_blocked": "ブロックされています。リセットするにはPUKを使用してください",
@ -330,6 +347,7 @@
"l_warning_default_puk": "警告: デフォルトのPUKが使用されています",
"l_default_pin_used": "デフォルトのPINが使用されています",
"l_default_puk_used": "既定のPUKを使用",
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "パスワード",
@ -339,6 +357,7 @@
"s_show_password": "パスワードを表示",
"s_hide_password": "パスワードを非表示",
"l_optional_password_protection": "オプションのパスワード保護",
"l_password_protection": null,
"s_new_password": "新しいパスワード",
"s_current_password": "現在のパスワード",
"s_confirm_password": "パスワードを確認",
@ -352,6 +371,7 @@
"l_keystore_unavailable": "OSのキーストアを利用できません",
"l_remember_pw_failed": "パスワードを記憶できませんでした",
"l_unlock_first": "最初にパスワードでロックを解除",
"l_set_password_first": null,
"l_enter_oath_pw": "YubiKeyのOATHパスワードを入力",
"p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります。",
"p_enter_new_password": "新しいパスワードを入力してください。パスワードには文字、数字、特殊文字を含めることができます。",
@ -410,6 +430,7 @@
"s_pin_account": "アカウントをピン留めする",
"s_unpin_account": "アカウントのピン留めを解除",
"s_no_pinned_accounts": "ピン留めされたアカウントはありません",
"s_pinned": null,
"l_pin_account_desc": "重要なアカウントをまとめて保持",
"s_rename_account": "アカウント名を変更",
"l_rename_account_desc": "アカウントの発行者/名前を編集",
@ -885,6 +906,7 @@
"@_key_customization": {},
"s_set_label": "ラベルを設定",
"s_set_color": null,
"s_change_label": "ラベルを変更",
"s_color": "色",
"p_set_will_add_custom_name": "これにより、YubiKey にカスタム名を付けることができます。",

View File

@ -30,6 +30,9 @@
"s_delete": "Usuń",
"s_move": null,
"s_quit": "Wyjdź",
"s_enable": null,
"s_enabled": null,
"s_disabled": null,
"s_status": "Status",
"s_unlock": "Odblokuj",
"s_calculate": "Oblicz",
@ -45,6 +48,8 @@
"s_hide_window": "Ukryj okno",
"s_expand_navigation": null,
"s_collapse_navigation": null,
"s_expand_sidebar": null,
"s_collapse_sidebar": null,
"q_rename_target": "Zmienić nazwę {label}?",
"@q_rename_target": {
"placeholders": {
@ -121,6 +126,12 @@
"s_light_mode": "Jasny",
"s_dark_mode": "Ciemny",
"@_layout": {},
"s_list_layout": null,
"s_grid_layout": null,
"s_mixed_layout": null,
"s_select_layout": null,
"@_yubikey_selection": {},
"s_select_to_scan": "Wybierz, aby skanować",
"s_hide_device": "Ukryj urządzenie",
@ -149,6 +160,8 @@
}
},
"l_firmware_version": null,
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "Podłącz klucz YubiKey",
@ -301,6 +314,10 @@
"common_pin": {}
}
},
"s_ep_attestation": null,
"s_ep_attestation_enabled": null,
"s_enable_ep_attestation": null,
"p_enable_ep_attestation_desc": 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ć",
@ -330,6 +347,7 @@
"l_warning_default_puk": null,
"l_default_pin_used": null,
"l_default_puk_used": null,
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Hasło",
@ -339,6 +357,7 @@
"s_show_password": "Pokaż hasło",
"s_hide_password": "Ukryj hasło",
"l_optional_password_protection": "Opcjonalna ochrona hasłem",
"l_password_protection": null,
"s_new_password": "Nowe hasło",
"s_current_password": "Aktualne hasło",
"s_confirm_password": "Potwierdź hasło",
@ -352,6 +371,7 @@
"l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny",
"l_remember_pw_failed": "Nie udało się zapamiętać hasła",
"l_unlock_first": "Najpierw odblokuj hasłem",
"l_set_password_first": null,
"l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey",
"p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
"p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.",
@ -410,6 +430,7 @@
"s_pin_account": "Przypnij konto",
"s_unpin_account": "Odepnij konto",
"s_no_pinned_accounts": "Brak przypiętych kont",
"s_pinned": null,
"l_pin_account_desc": "Przechowuj ważne konta razem",
"s_rename_account": "Zmień nazwę konta",
"l_rename_account_desc": "Edytuj wydawcę/nazwę konta",
@ -885,6 +906,7 @@
"@_key_customization": {},
"s_set_label": null,
"s_set_color": null,
"s_change_label": null,
"s_color": null,
"p_set_will_add_custom_name": null,

View File

@ -15,6 +15,7 @@
*/
import 'dart:developer' as developer;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -27,6 +28,7 @@ import 'app/state.dart';
import 'core/state.dart';
import 'desktop/init.dart' as desktop;
import 'error_page.dart';
import 'version.dart';
final _log = Logger('main');
@ -43,6 +45,12 @@ void main(List<String> argv) async {
_initializeDebugLogging();
throw UnimplementedError('Platform not supported');
}
_log.info('Running Yubico Authenticator...', {
'app_version': version,
'dart': Platform.version,
'os': Platform.operatingSystem,
'os_version': Platform.operatingSystemVersion,
});
runApp(initializedApp);
} catch (e) {
_log.warning('Platform initialization failed: $e');

View File

@ -78,6 +78,8 @@ class DeviceConfig with _$DeviceConfig {
@freezed
class DeviceInfo with _$DeviceInfo {
const DeviceInfo._(); // Added constructor
factory DeviceInfo(
DeviceConfig config,
int? serial,
@ -88,8 +90,17 @@ class DeviceInfo with _$DeviceInfo {
bool isFips,
bool isSky,
bool pinComplexity,
int fipsCapable) = _DeviceInfo;
int fipsCapable,
int fipsApproved,
int resetBlocked) = _DeviceInfo;
factory DeviceInfo.fromJson(Map<String, dynamic> json) =>
_$DeviceInfoFromJson(json);
/// Gets the tuple fipsCapable, fipsApproved for the given capability.
(bool fipsCapable, bool fipsApproved) getFipsStatus(Capability capability) {
final capable = fipsCapable & capability.value != 0;
final approved = capable && fipsApproved & capability.value != 0;
return (capable, approved);
}
}

View File

@ -247,6 +247,8 @@ mixin _$DeviceInfo {
bool get isSky => throw _privateConstructorUsedError;
bool get pinComplexity => throw _privateConstructorUsedError;
int get fipsCapable => throw _privateConstructorUsedError;
int get fipsApproved => throw _privateConstructorUsedError;
int get resetBlocked => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -270,7 +272,9 @@ abstract class $DeviceInfoCopyWith<$Res> {
bool isFips,
bool isSky,
bool pinComplexity,
int fipsCapable});
int fipsCapable,
int fipsApproved,
int resetBlocked});
$DeviceConfigCopyWith<$Res> get config;
$VersionCopyWith<$Res> get version;
@ -299,6 +303,8 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo>
Object? isSky = null,
Object? pinComplexity = null,
Object? fipsCapable = null,
Object? fipsApproved = null,
Object? resetBlocked = null,
}) {
return _then(_value.copyWith(
config: null == config
@ -341,6 +347,14 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo>
? _value.fipsCapable
: fipsCapable // ignore: cast_nullable_to_non_nullable
as int,
fipsApproved: null == fipsApproved
? _value.fipsApproved
: fipsApproved // ignore: cast_nullable_to_non_nullable
as int,
resetBlocked: null == resetBlocked
? _value.resetBlocked
: resetBlocked // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
@ -379,7 +393,9 @@ abstract class _$$DeviceInfoImplCopyWith<$Res>
bool isFips,
bool isSky,
bool pinComplexity,
int fipsCapable});
int fipsCapable,
int fipsApproved,
int resetBlocked});
@override
$DeviceConfigCopyWith<$Res> get config;
@ -408,6 +424,8 @@ class __$$DeviceInfoImplCopyWithImpl<$Res>
Object? isSky = null,
Object? pinComplexity = null,
Object? fipsCapable = null,
Object? fipsApproved = null,
Object? resetBlocked = null,
}) {
return _then(_$DeviceInfoImpl(
null == config
@ -450,13 +468,21 @@ class __$$DeviceInfoImplCopyWithImpl<$Res>
? _value.fipsCapable
: fipsCapable // ignore: cast_nullable_to_non_nullable
as int,
null == fipsApproved
? _value.fipsApproved
: fipsApproved // ignore: cast_nullable_to_non_nullable
as int,
null == resetBlocked
? _value.resetBlocked
: resetBlocked // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$DeviceInfoImpl implements _DeviceInfo {
class _$DeviceInfoImpl extends _DeviceInfo {
_$DeviceInfoImpl(
this.config,
this.serial,
@ -467,8 +493,11 @@ class _$DeviceInfoImpl implements _DeviceInfo {
this.isFips,
this.isSky,
this.pinComplexity,
this.fipsCapable)
: _supportedCapabilities = supportedCapabilities;
this.fipsCapable,
this.fipsApproved,
this.resetBlocked)
: _supportedCapabilities = supportedCapabilities,
super._();
factory _$DeviceInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$DeviceInfoImplFromJson(json);
@ -500,10 +529,14 @@ class _$DeviceInfoImpl implements _DeviceInfo {
final bool pinComplexity;
@override
final int fipsCapable;
@override
final int fipsApproved;
@override
final int resetBlocked;
@override
String toString() {
return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity, fipsCapable: $fipsCapable)';
return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity, fipsCapable: $fipsCapable, fipsApproved: $fipsApproved, resetBlocked: $resetBlocked)';
}
@override
@ -525,7 +558,11 @@ class _$DeviceInfoImpl implements _DeviceInfo {
(identical(other.pinComplexity, pinComplexity) ||
other.pinComplexity == pinComplexity) &&
(identical(other.fipsCapable, fipsCapable) ||
other.fipsCapable == fipsCapable));
other.fipsCapable == fipsCapable) &&
(identical(other.fipsApproved, fipsApproved) ||
other.fipsApproved == fipsApproved) &&
(identical(other.resetBlocked, resetBlocked) ||
other.resetBlocked == resetBlocked));
}
@JsonKey(ignore: true)
@ -541,7 +578,9 @@ class _$DeviceInfoImpl implements _DeviceInfo {
isFips,
isSky,
pinComplexity,
fipsCapable);
fipsCapable,
fipsApproved,
resetBlocked);
@JsonKey(ignore: true)
@override
@ -557,7 +596,7 @@ class _$DeviceInfoImpl implements _DeviceInfo {
}
}
abstract class _DeviceInfo implements DeviceInfo {
abstract class _DeviceInfo extends DeviceInfo {
factory _DeviceInfo(
final DeviceConfig config,
final int? serial,
@ -568,7 +607,10 @@ abstract class _DeviceInfo implements DeviceInfo {
final bool isFips,
final bool isSky,
final bool pinComplexity,
final int fipsCapable) = _$DeviceInfoImpl;
final int fipsCapable,
final int fipsApproved,
final int resetBlocked) = _$DeviceInfoImpl;
_DeviceInfo._() : super._();
factory _DeviceInfo.fromJson(Map<String, dynamic> json) =
_$DeviceInfoImpl.fromJson;
@ -594,6 +636,10 @@ abstract class _DeviceInfo implements DeviceInfo {
@override
int get fipsCapable;
@override
int get fipsApproved;
@override
int get resetBlocked;
@override
@JsonKey(ignore: true)
_$$DeviceInfoImplCopyWith<_$DeviceInfoImpl> get copyWith =>
throw _privateConstructorUsedError;

View File

@ -46,6 +46,8 @@ _$DeviceInfoImpl _$$DeviceInfoImplFromJson(Map<String, dynamic> json) =>
json['is_sky'] as bool,
json['pin_complexity'] as bool,
(json['fips_capable'] as num).toInt(),
(json['fips_approved'] as num).toInt(),
(json['reset_blocked'] as num).toInt(),
);
Map<String, dynamic> _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) =>
@ -61,6 +63,8 @@ Map<String, dynamic> _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) =>
'is_sky': instance.isSky,
'pin_complexity': instance.pinComplexity,
'fips_capable': instance.fipsCapable,
'fips_approved': instance.fipsApproved,
'reset_blocked': instance.resetBlocked,
};
const _$FormFactorEnumMap = {

View File

@ -258,3 +258,5 @@ class CredentialData with _$CredentialData {
},
);
}
enum OathLayout { list, grid, mixed }

View File

@ -13,7 +13,7 @@ _$OathCredentialImpl _$$OathCredentialImplFromJson(Map<String, dynamic> json) =>
const _IssuerConverter().fromJson(json['issuer'] as String?),
json['name'] as String,
$enumDecode(_$OathTypeEnumMap, json['oath_type']),
json['period'] as int,
(json['period'] as num).toInt(),
json['touch_required'] as bool,
);
@ -37,8 +37,8 @@ const _$OathTypeEnumMap = {
_$OathCodeImpl _$$OathCodeImplFromJson(Map<String, dynamic> json) =>
_$OathCodeImpl(
json['value'] as String,
json['valid_from'] as int,
json['valid_to'] as int,
(json['valid_from'] as num).toInt(),
(json['valid_to'] as num).toInt(),
);
Map<String, dynamic> _$$OathCodeImplToJson(_$OathCodeImpl instance) =>
@ -98,9 +98,9 @@ _$CredentialDataImpl _$$CredentialDataImplFromJson(Map<String, dynamic> json) =>
hashAlgorithm:
$enumDecodeNullable(_$HashAlgorithmEnumMap, json['hash_algorithm']) ??
defaultHashAlgorithm,
digits: json['digits'] as int? ?? defaultDigits,
period: json['period'] as int? ?? defaultPeriod,
counter: json['counter'] as int? ?? defaultCounter,
digits: (json['digits'] as num?)?.toInt() ?? defaultDigits,
period: (json['period'] as num?)?.toInt() ?? defaultPeriod,
counter: (json['counter'] as num?)?.toInt() ?? defaultCounter,
);
Map<String, dynamic> _$$CredentialDataImplToJson(

View File

@ -38,6 +38,53 @@ class AccountsSearchNotifier extends StateNotifier<String> {
}
}
final oathLayoutProvider =
StateNotifierProvider.autoDispose<OathLayoutNotfier, OathLayout>((ref) {
final device = ref.watch(currentDeviceProvider);
List<OathPair> credentials = device != null
? ref.read(filteredCredentialsProvider(
ref.read(credentialListProvider(device.path)) ?? []))
: [];
final favorites = ref.watch(favoritesProvider);
final pinnedCreds =
credentials.where((entry) => favorites.contains(entry.credential.id));
return OathLayoutNotfier('OATH_STATE_LAYOUT', ref.watch(prefProvider),
credentials, pinnedCreds.toList());
});
class OathLayoutNotfier extends StateNotifier<OathLayout> {
final String _key;
final SharedPreferences _prefs;
OathLayoutNotfier(this._key, this._prefs, List<OathPair> credentials,
List<OathPair> pinnedCredentials)
: super(
_fromName(_prefs.getString(_key), credentials, pinnedCredentials));
void setLayout(OathLayout layout) {
state = layout;
_prefs.setString(_key, layout.name);
}
static OathLayout _fromName(String? name, List<OathPair> credentials,
List<OathPair> pinnedCredentials) {
final layout = OathLayout.values.firstWhere(
(element) => element.name == name,
orElse: () => OathLayout.list,
);
// Default to list view if current key does not have
// pinned credentials
if (layout == OathLayout.mixed) {
if (pinnedCredentials.isEmpty) {
return OathLayout.list;
}
if (pinnedCredentials.length == credentials.length) {
return OathLayout.grid;
}
}
return layout;
}
}
final oathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
() => throw UnimplementedError(),

View File

@ -26,7 +26,6 @@ import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../core/state.dart';
import '../../widgets/circle_timer.dart';
import '../../widgets/custom_icons.dart';
import '../features.dart' as features;
import '../keys.dart' as keys;
import '../models.dart';
@ -90,7 +89,7 @@ class AccountHelper {
ActionItem(
key: keys.togglePinAction,
feature: features.accountsPin,
icon: pinned ? pushPinStrokeIcon : const Icon(Symbols.push_pin),
icon: Icon(pinned ? Symbols.keep_off : Symbols.keep),
title: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
subtitle: l10n.l_pin_account_desc,
intent: TogglePinIntent(credential),

View File

@ -18,6 +18,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/flex_box.dart';
import '../models.dart';
import '../state.dart';
import 'account_view.dart';
@ -32,6 +33,9 @@ class AccountList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final labelStyle =
theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.primary);
final credentials = ref.watch(filteredCredentialsProvider(accounts));
final favorites = ref.watch(favoritesProvider);
if (credentials.isEmpty) {
@ -45,32 +49,71 @@ class AccountList extends ConsumerWidget {
final creds =
credentials.where((entry) => !favorites.contains(entry.credential.id));
final oathLayout = ref.watch(oathLayoutProvider);
final pinnedLayout =
(oathLayout == OathLayout.grid || oathLayout == OathLayout.mixed)
? FlexLayout.grid
: FlexLayout.list;
final normalLayout =
oathLayout == OathLayout.grid ? FlexLayout.grid : FlexLayout.list;
return FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: Column(
children: [
...pinnedCreds.map(
(entry) => AccountView(
entry.credential,
expanded: expanded,
selected: entry.credential == selected,
),
),
if (pinnedCreds.isNotEmpty && creds.isNotEmpty)
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (pinnedCreds.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(left: 18, bottom: 8),
child: Text(l10n.s_pinned, style: labelStyle),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: FlexBox<OathPair>(
items: pinnedCreds.toList(),
itemBuilder: (value) => AccountView(
value.credential,
expanded: expanded,
selected: value.credential == selected,
large: pinnedLayout == FlexLayout.grid,
),
cellMinWidth: 250,
spacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0,
runSpacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0,
layout: pinnedLayout,
),
),
],
if (pinnedCreds.isNotEmpty && creds.isNotEmpty) ...[
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 18, bottom: 8),
child: Text(
l10n.s_accounts,
style: labelStyle,
),
),
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Divider(
color: Theme.of(context).colorScheme.secondaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: FlexBox<OathPair>(
items: creds.toList(),
itemBuilder: (value) => AccountView(
value.credential,
expanded: expanded,
selected: value.credential == selected,
large: normalLayout == FlexLayout.grid,
),
cellMinWidth: 250,
spacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0,
runSpacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0,
layout: normalLayout,
),
),
...creds.map(
(entry) => AccountView(
entry.credential,
expanded: expanded,
selected: entry.credential == selected,
),
),
],
],
),
),
);
}

View File

@ -30,8 +30,12 @@ class AccountView extends ConsumerStatefulWidget {
final OathCredential credential;
final bool expanded;
final bool selected;
final bool large;
const AccountView(this.credential,
{super.key, required this.expanded, required this.selected});
{super.key,
required this.expanded,
required this.selected,
this.large = false});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _AccountViewState();
@ -116,6 +120,95 @@ class _AccountViewState extends ConsumerState<AccountView> {
? CopyIntent<OathCredential>(credential)
: null,
buildPopupActions: (_) => helper.buildActions(),
itemBuilder: widget.large
? (context) {
return ListTile(
mouseCursor: !(isDesktop && !widget.expanded)
? SystemMouseCursors.click
: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
selectedTileColor:
Theme.of(context).colorScheme.secondaryContainer,
selectedColor:
Theme.of(context).colorScheme.onSecondaryContainer,
selected: widget.selected,
tileColor: Theme.of(context).hoverColor,
contentPadding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
title: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountIcon(
issuer: credential.issuer,
defaultWidget: circleAvatar),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
helper.title,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface),
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
if (subtitle != null)
Text(
subtitle,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
],
),
)
],
),
const SizedBox(height: 8.0),
Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
helper.code != null
? FilledButton.tonalIcon(
icon: helper.buildCodeIcon(),
label: helper.buildCodeLabel(),
style: buttonStyle,
onPressed:
Actions.handler(context, openIntent),
)
: FilledButton.tonal(
style: buttonStyle,
onPressed:
Actions.handler(context, openIntent),
child: helper.buildCodeIcon()),
],
),
),
],
),
);
}
: null,
);
}
}

View File

@ -23,6 +23,7 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../management/models.dart';
import '../features.dart' as features;
import '../icon_provider/icon_pack_dialog.dart';
import '../keys.dart' as keys;
@ -39,6 +40,28 @@ Widget oathBuildActions(
}) {
final l10n = AppLocalizations.of(context)!;
final capacity = oathState.capacity;
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.oath) ??
(false, false);
final String? subtitle;
final bool enabled;
if (used == null) {
subtitle = l10n.l_unlock_first;
enabled = false;
} else if (fipsCapable & !fipsApproved) {
subtitle = l10n.l_set_password_first;
enabled = false;
} else if (capacity != null) {
subtitle = l10n.l_accounts_used(used, capacity);
enabled = capacity > used;
} else {
subtitle = null;
enabled = true;
}
return Column(
children: [
@ -47,14 +70,10 @@ Widget oathBuildActions(
feature: features.actionsAdd,
key: keys.addAccountAction,
title: l10n.s_add_account,
subtitle: used == null
? l10n.l_unlock_first
: (capacity != null
? l10n.l_accounts_used(used, capacity)
: null),
subtitle: subtitle,
actionStyle: ActionStyle.primary,
icon: const Icon(Symbols.person_add_alt),
onTap: used != null && (capacity == null || capacity > used)
onTap: enabled
? (context) async {
Navigator.of(context).popUntil((route) => route.isFirst);
await addOathAccount(context, ref, devicePath, oathState);
@ -82,7 +101,7 @@ Widget oathBuildActions(
feature: features.actionsPassword,
title:
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
subtitle: l10n.l_optional_password_protection,
subtitle: l10n.l_password_protection,
icon: const Icon(Symbols.password),
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2023 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.
@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
import '../../widgets/focus_utils.dart';
@ -33,6 +34,7 @@ import '../state.dart';
class ManagePasswordDialog extends ConsumerStatefulWidget {
final DevicePath path;
final OathState state;
const ManagePasswordDialog(this.path, this.state, {super.key});
@override
@ -43,6 +45,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
final _currentPasswordController = TextEditingController();
final _currentPasswordFocus = FocusNode();
final _newPasswordFocus = FocusNode();
final _confirmPasswordFocus = FocusNode();
String _newPassword = '';
String _confirmPassword = '';
bool _currentIsWrong = false;
@ -54,6 +58,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
void dispose() {
_currentPasswordController.dispose();
_currentPasswordFocus.dispose();
_newPasswordFocus.dispose();
_confirmPasswordFocus.dispose();
super.dispose();
}
@ -82,12 +88,22 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
@override
Widget build(BuildContext context) {
final fipsCapable = ref.watch(currentDeviceDataProvider).maybeWhen(
data: (data) => data.info.getFipsStatus(Capability.oath).$1,
orElse: () => false);
final l10n = AppLocalizations.of(context)!;
final isValid = !_currentIsWrong &&
_newPassword.isNotEmpty &&
_newPassword == _confirmPassword &&
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
final newPasswordEnabled =
!widget.state.hasKey || _currentPasswordController.text.isNotEmpty;
final confirmPasswordEnabled =
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty) &&
_newPassword.isNotEmpty;
return ResponsiveDialog(
title: Text(
widget.state.hasKey ? l10n.s_manage_password : l10n.s_set_password),
@ -137,42 +153,52 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
_currentIsWrong = false;
});
},
onSubmitted: (_) {
if (_currentPasswordController.text.isEmpty) {
_currentPasswordFocus.requestFocus();
} else {
_newPasswordFocus.requestFocus();
}
},
).init(),
Wrap(
spacing: 4.0,
runSpacing: 8.0,
children: [
OutlinedButton(
key: keys.removePasswordButton,
onPressed: _currentPasswordController.text.isNotEmpty &&
!_currentIsWrong
? () async {
final result = await ref
.read(oathStateProvider(widget.path).notifier)
.unsetPassword(_currentPasswordController.text);
if (result) {
if (mounted) {
await ref.read(withContextProvider)(
(context) async {
Navigator.of(context).pop();
showMessage(context, l10n.s_password_removed);
if (!fipsCapable)
OutlinedButton(
key: keys.removePasswordButton,
onPressed: _currentPasswordController.text.isNotEmpty &&
!_currentIsWrong
? () async {
final result = await ref
.read(oathStateProvider(widget.path).notifier)
.unsetPassword(
_currentPasswordController.text);
if (result) {
if (mounted) {
await ref.read(withContextProvider)(
(context) async {
Navigator.of(context).pop();
showMessage(
context, l10n.s_password_removed);
});
}
} else {
_currentPasswordController.selection =
TextSelection(
baseOffset: 0,
extentOffset: _currentPasswordController
.text.length);
_currentPasswordFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
}
} else {
_currentPasswordController.selection =
TextSelection(
baseOffset: 0,
extentOffset: _currentPasswordController
.text.length);
_currentPasswordFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
}
}
: null,
child: Text(l10n.s_remove_password),
),
: null,
child: Text(l10n.s_remove_password),
),
if (widget.state.remembered)
OutlinedButton(
child: Text(l10n.s_clear_saved_password),
@ -197,24 +223,27 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
autofocus: !widget.state.hasKey,
obscureText: _isObscureNew,
autofillHints: const [AutofillHints.newPassword],
focusNode: _newPasswordFocus,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_new_password,
prefixIcon: const Icon(Symbols.password),
suffixIcon: IconButton(
icon: Icon(_isObscureNew
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureNew = !_isObscureNew;
});
},
tooltip: _isObscureNew
? l10n.s_show_password
: l10n.s_hide_password),
enabled: !widget.state.hasKey ||
_currentPasswordController.text.isNotEmpty,
suffixIcon: ExcludeFocusTraversal(
excluding: !newPasswordEnabled,
child: IconButton(
icon: Icon(_isObscureNew
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureNew = !_isObscureNew;
});
},
tooltip: _isObscureNew
? l10n.s_show_password
: l10n.s_hide_password),
),
enabled: newPasswordEnabled,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -223,34 +252,38 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
});
},
onSubmitted: (_) {
if (isValid) {
_submit();
if (_newPassword.isNotEmpty) {
_confirmPasswordFocus.requestFocus();
} else if (_newPassword.isEmpty) {
_newPasswordFocus.requestFocus();
}
},
).init(),
AppTextField(
key: keys.confirmPasswordField,
obscureText: _isObscureConfirm,
focusNode: _confirmPasswordFocus,
autofillHints: const [AutofillHints.newPassword],
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_confirm_password,
prefixIcon: const Icon(Symbols.password),
suffixIcon: IconButton(
icon: Icon(_isObscureConfirm
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureConfirm = !_isObscureConfirm;
});
},
tooltip: _isObscureConfirm
? l10n.s_show_password
: l10n.s_hide_password),
enabled: (!widget.state.hasKey ||
_currentPasswordController.text.isNotEmpty) &&
_newPassword.isNotEmpty,
suffixIcon: ExcludeFocusTraversal(
excluding: !confirmPasswordEnabled,
child: IconButton(
icon: Icon(_isObscureConfirm
? Symbols.visibility
: Symbols.visibility_off),
onPressed: () {
setState(() {
_isObscureConfirm = !_isObscureConfirm;
});
},
tooltip: _isObscureConfirm
? l10n.s_show_password
: l10n.s_hide_password),
),
enabled: confirmPasswordEnabled,
errorText: _newPassword.length == _confirmPassword.length &&
_newPassword != _confirmPassword
? l10n.l_password_mismatch
@ -266,6 +299,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
onSubmitted: (_) {
if (isValid) {
_submit();
} else {
_confirmPasswordFocus.requestFocus();
}
},
).init(),

View File

@ -53,6 +53,19 @@ import 'key_actions.dart';
import 'unlock_form.dart';
import 'utils.dart';
extension on OathLayout {
IconData get _icon => switch (this) {
OathLayout.list => Symbols.list,
OathLayout.grid => Symbols.grid_view,
OathLayout.mixed => Symbols.vertical_split
};
String getDisplayName(AppLocalizations l10n) => switch (this) {
OathLayout.list => l10n.s_list_layout,
OathLayout.grid => l10n.s_grid_layout,
OathLayout.mixed => l10n.s_mixed_layout
};
}
class OathScreen extends ConsumerWidget {
final DevicePath devicePath;
@ -123,6 +136,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
late FocusNode searchFocus;
late TextEditingController searchController;
OathCredential? _selected;
bool _canRequestFocus = true;
@override
void initState() {
@ -376,60 +390,171 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
}
return KeyEventResult.ignored;
},
child: Builder(builder: (context) {
child: LayoutBuilder(builder: (context, constraints) {
final width = constraints.maxWidth;
final textTheme = Theme.of(context).textTheme;
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: AppTextFormField(
key: searchField,
controller: searchController,
focusNode: searchFocus,
// Use the default style, but with a smaller font size:
style: textTheme.titleMedium
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
decoration: AppInputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(48),
borderSide: BorderSide(
width: 0,
style: searchFocus.hasFocus
? BorderStyle.solid
: BorderStyle.none,
return Consumer(
builder: (context, ref, child) {
final credentials = ref.watch(filteredCredentialsProvider(
ref.watch(credentialListProvider(widget.devicePath)) ??
[]));
final favorites = ref.watch(favoritesProvider);
final pinnedCreds = credentials
.where((entry) => favorites.contains(entry.credential.id));
final availableLayouts = pinnedCreds.isEmpty ||
pinnedCreds.length == credentials.length
? OathLayout.values
.where((element) => element != OathLayout.mixed)
: OathLayout.values;
final oathLayout = ref.watch(oathLayoutProvider);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 8.0),
child: AppTextFormField(
key: searchField,
controller: searchController,
canRequestFocus: _canRequestFocus,
focusNode: searchFocus,
// Use the default style, but with a smaller font size:
style: textTheme.titleMedium
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
decoration: AppInputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(48),
borderSide: BorderSide(
width: 0,
style: searchFocus.hasFocus
? BorderStyle.solid
: BorderStyle.none,
),
),
contentPadding: const EdgeInsets.all(16),
fillColor: Theme.of(context).hoverColor,
filled: true,
hintText: l10n.s_search_accounts,
isDense: true,
prefixIcon: const Padding(
padding: EdgeInsetsDirectional.only(start: 8.0),
child: Icon(Icons.search_outlined),
),
suffixIcons: [
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
iconSize: 16,
onPressed: () {
searchController.clear();
ref
.read(accountsSearchProvider.notifier)
.setFilter('');
setState(() {});
},
),
if (searchController.text.isEmpty) ...[
if (width >= 450)
...availableLayouts.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(oathLayoutProvider.notifier)
.setLayout(e);
},
icon: Icon(
e._icon,
color: e == oathLayout
? Theme.of(context).colorScheme.primary
: null,
),
),
),
),
if (width < 450)
MouseRegion(
onEnter: (event) {
if (!searchFocus.hasFocus) {
setState(() {
_canRequestFocus = false;
});
}
},
onExit: (event) {
setState(() {
_canRequestFocus = true;
});
},
child: PopupMenuButton(
constraints: const BoxConstraints.tightFor(),
tooltip: 'Select layout',
popUpAnimationStyle:
AnimationStyle(duration: Duration.zero),
icon: Icon(
oathLayout._icon,
color: Theme.of(context).colorScheme.primary,
),
itemBuilder: (context) => [
...availableLayouts.map(
(e) => PopupMenuItem(
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Tooltip(
message: e.getDisplayName(l10n),
child: Icon(
e._icon,
color: e == oathLayout
? Theme.of(context)
.colorScheme
.primary
: null,
),
),
],
),
onTap: () {
ref
.read(oathLayoutProvider.notifier)
.setLayout(e);
},
),
)
],
),
)
]
],
),
),
contentPadding: const EdgeInsets.all(16),
fillColor: Theme.of(context).hoverColor,
filled: true,
hintText: l10n.s_search_accounts,
isDense: true,
prefixIcon: const Padding(
padding: EdgeInsetsDirectional.only(start: 8.0),
child: Icon(Icons.search_outlined),
),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
iconSize: 16,
onPressed: () {
searchController.clear();
ref
.read(accountsSearchProvider.notifier)
.setFilter('');
setState(() {});
},
)
: null,
),
onChanged: (value) {
ref.read(accountsSearchProvider.notifier).setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context).focusInDirection(TraversalDirection.down);
},
).init(),
onChanged: (value) {
ref
.read(accountsSearchProvider.notifier)
.setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context)
.focusInDirection(TraversalDirection.down);
},
).init(),
);
},
);
}),
),

View File

@ -180,13 +180,17 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
return null;
}),
},
child: Column(children: [
...otpState.slots.map((e) => _SlotListItem(
e,
expanded: expanded,
selected: e == selected,
))
]),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(children: [
...otpState.slots.map((e) => _SlotListItem(
e,
expanded: expanded,
selected: e == selected,
))
]),
),
);
},
),

View File

@ -9,8 +9,8 @@ part of 'models.dart';
_$PinMetadataImpl _$$PinMetadataImplFromJson(Map<String, dynamic> json) =>
_$PinMetadataImpl(
json['default_value'] as bool,
json['total_attempts'] as int,
json['attempts_remaining'] as int,
(json['total_attempts'] as num).toInt(),
(json['attempts_remaining'] as num).toInt(),
);
Map<String, dynamic> _$$PinMetadataImplToJson(_$PinMetadataImpl instance) =>
@ -113,7 +113,7 @@ _$PivStateImpl _$$PivStateImplFromJson(Map<String, dynamic> json) =>
authenticated: json['authenticated'] as bool,
derivedKey: json['derived_key'] as bool,
storedKey: json['stored_key'] as bool,
pinAttempts: json['pin_attempts'] as int,
pinAttempts: (json['pin_attempts'] as num).toInt(),
chuid: json['chuid'] as String?,
ccc: json['ccc'] as String?,
metadata: json['metadata'] == null
@ -157,7 +157,7 @@ Map<String, dynamic> _$$CertInfoImplToJson(_$CertInfoImpl instance) =>
_$PivSlotImpl _$$PivSlotImplFromJson(Map<String, dynamic> json) =>
_$PivSlotImpl(
slot: SlotId.fromJson(json['slot'] as int),
slot: SlotId.fromJson((json['slot'] as num).toInt()),
metadata: json['metadata'] == null
? null
: SlotMetadata.fromJson(json['metadata'] as Map<String, dynamic>),

View File

@ -310,7 +310,16 @@ class PivActions extends ConsumerWidget {
}
List<ActionItem> buildSlotActions(
PivState pivState, PivSlot slot, AppLocalizations l10n) {
PivState pivState, PivSlot slot, bool fipsUnready, AppLocalizations l10n) {
if (fipsUnready) {
// TODO: Decide on final look and move strings to .arb file.
return [
ActionItem(
icon: const Icon(Symbols.add),
title: 'Provision slot',
subtitle: 'Change from default PIN/PUK/Management key first'),
];
}
final hasCert = slot.certInfo != null;
final hasKey = slot.metadata != null;
final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7);

View File

@ -25,6 +25,7 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../core/models.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
import '../../widgets/app_text_form_field.dart';
@ -176,6 +177,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
? currentKeyOrPin.length >= 4
: currentKeyOrPin.length == currentType.keyLength * 2;
final newLenOk = _keyController.text.length == hexLength;
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.piv) ??
(false, false);
final fipsUnready = fipsCapable && !fipsApproved;
final managementKeyTypes = ManagementKeyType.values.toList();
if (fipsCapable) {
managementKeyTypes.remove(ManagementKeyType.tdes);
}
return ResponsiveDialog(
title: Text(l10n.l_change_management_key),
@ -334,7 +346,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
children: [
if (widget.pivState.metadata != null)
ChoiceFilterChip<ManagementKeyType>(
items: ManagementKeyType.values,
items: managementKeyTypes,
value: _keyType,
selected: _keyType != currentType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
@ -344,16 +356,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
});
},
),
FilterChip(
key: keys.pinLockManagementKeyChip,
label: Text(l10n.s_protect_key),
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
if (!fipsUnready)
FilterChip(
key: keys.pinLockManagementKeyChip,
label: Text(l10n.s_protect_key),
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
]),
]
.map((e) => Padding(

View File

@ -165,8 +165,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
final isBio = [FormFactor.usbABio, FormFactor.usbCBio]
.contains(deviceData?.info.formFactor);
final fipsCapable = deviceData?.info.fipsCapable ?? 0;
final isFipsCapable = fipsCapable & Capability.piv.value != 0;
final isFipsCapable =
deviceData?.info.getFipsStatus(Capability.piv).$1 ?? false;
// Old YubiKeys allowed a 4 digit PIN
final currentMinPinLen = isFipsCapable

View File

@ -24,6 +24,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../app/views/app_failure_page.dart';
import '../../app/views/app_list_item.dart';
@ -57,6 +58,14 @@ class _PivScreenState extends ConsumerState<PivScreen> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider);
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.piv) ??
(false, false);
final fipsUnready = fipsCapable && !fipsApproved;
return ref.watch(pivStateProvider(widget.devicePath)).when(
loading: () => MessagePage(
title: l10n.s_certificates,
@ -168,8 +177,8 @@ class _PivScreenState extends ConsumerState<PivScreen> {
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions:
buildSlotActions(pivState, selected, l10n),
actions: buildSlotActions(
pivState, selected, fipsUnready, l10n),
),
],
)
@ -200,25 +209,30 @@ class _PivScreenState extends ConsumerState<PivScreen> {
return null;
}),
},
child: Column(
children: [
...normalSlots.map(
(e) => _CertificateListItem(
pivState,
e,
expanded: expanded,
selected: e == selected,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
children: [
...normalSlots.map(
(e) => _CertificateListItem(
pivState,
e,
expanded: expanded,
selected: e == selected,
fipsUnready: fipsUnready,
),
),
),
...shownRetiredSlots.map(
(e) => _CertificateListItem(
pivState,
e,
expanded: expanded,
selected: e == selected,
),
)
],
...shownRetiredSlots.map(
(e) => _CertificateListItem(
pivState,
e,
expanded: expanded,
selected: e == selected,
fipsUnready: fipsUnready,
),
)
],
),
),
);
},
@ -235,9 +249,12 @@ class _CertificateListItem extends ConsumerWidget {
final PivSlot pivSlot;
final bool expanded;
final bool selected;
final bool fipsUnready;
const _CertificateListItem(this.pivState, this.pivSlot,
{required this.expanded, required this.selected});
{required this.expanded,
required this.selected,
required this.fipsUnready});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -272,8 +289,8 @@ class _CertificateListItem extends ConsumerWidget {
),
tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot),
doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null,
buildPopupActions: hasFeature(features.slots)
? (context) => buildSlotActions(pivState, pivSlot, l10n)
buildPopupActions: hasFeature(features.slots) && !fipsUnready
? (context) => buildSlotActions(pivState, pivSlot, fipsUnready, l10n)
: null,
);
}

View File

@ -22,6 +22,7 @@ import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../app/views/fs_dialog.dart';
import '../../management/models.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
@ -34,12 +35,13 @@ class SlotDialog extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
if (node == null) {
var keyData = ref.watch(currentDeviceDataProvider).valueOrNull;
if (keyData == 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 devicePath = keyData.node.path;
final l10n = AppLocalizations.of(context)!;
final textTheme = Theme.of(context).textTheme;
@ -48,8 +50,11 @@ class SlotDialog extends ConsumerWidget {
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
final (fipsCapable, fipsApproved) =
keyData.info.getFipsStatus(Capability.piv);
final pivState = ref.watch(pivStateProvider(devicePath)).valueOrNull;
final slotData = ref.watch(pivSlotsProvider(devicePath).select((value) =>
value.whenOrNull(
data: (data) =>
data.firstWhere((element) => element.slot == pivSlot))));
@ -61,7 +66,7 @@ class SlotDialog extends ConsumerWidget {
final certInfo = slotData.certInfo;
final metadata = slotData.metadata;
return PivActions(
devicePath: node.path,
devicePath: devicePath,
pivState: pivState,
builder: (context) => ItemShortcuts(
item: slotData,
@ -113,7 +118,8 @@ class SlotDialog extends ConsumerWidget {
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions: buildSlotActions(pivState, slotData, l10n),
actions: buildSlotActions(
pivState, slotData, fipsCapable && !fipsApproved, l10n),
),
],
),

View File

@ -1,68 +0,0 @@
/*
* 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:material_symbols_icons/symbols.dart';
final Widget pushPinStrokeIcon = Builder(builder: (context) {
return CustomPaint(
painter: _StrikethroughPainter(IconTheme.of(context).color ?? Colors.black),
child: ClipPath(
clipper: _StrikethroughClipper(), child: const Icon(Symbols.push_pin)),
);
});
class _StrikethroughClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
Path path = Path()
..moveTo(0, 2)
..lineTo(0, size.height)
..lineTo(size.width - 2, size.height)
..lineTo(0, 2)
..moveTo(2, 0)
..lineTo(size.width, size.height - 2)
..lineTo(size.width, 0)
..lineTo(2, 0)
..close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return false;
}
}
class _StrikethroughPainter extends CustomPainter {
final Color color;
_StrikethroughPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 1.3;
canvas.drawLine(Offset(size.width * 0.15, size.height * 0.15),
Offset(size.width * 0.8, size.height * 0.8), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

111
lib/widgets/flex_box.dart Normal file
View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:material_symbols_icons/symbols.dart';
enum FlexLayout {
list,
grid;
IconData get icon => switch (this) {
FlexLayout.list => Symbols.list,
FlexLayout.grid => Symbols.grid_view
};
String getDisplayName(AppLocalizations l10n) => switch (this) {
FlexLayout.list => l10n.s_list_layout,
FlexLayout.grid => l10n.s_grid_layout
};
}
class FlexBox<T> extends StatelessWidget {
final List<T> items;
final Widget Function(T value) itemBuilder;
final FlexLayout layout;
final double cellMinWidth;
final double spacing;
final double runSpacing;
const FlexBox({
super.key,
required this.items,
required this.itemBuilder,
required this.cellMinWidth,
this.layout = FlexLayout.list,
this.spacing = 0.0,
this.runSpacing = 0.0,
});
int _getItemsPerRow(double width) {
// Calculate the maximum number of cells that can fit in one row
int cellsPerRow = (width / (cellMinWidth + spacing)).floor();
// Ensure there's at least one cell per row
if (cellsPerRow < 1) {
cellsPerRow = 1;
}
// Calculate the total width needed for the calculated number of cells and spacing
double totalWidthNeeded =
cellsPerRow * cellMinWidth + (cellsPerRow - 1) * spacing;
// Adjust the number of cells per row if the calculated total width exceeds the available width
if (totalWidthNeeded > width) {
cellsPerRow = cellsPerRow - 1 > 0 ? cellsPerRow - 1 : 1;
}
return cellsPerRow;
}
List<List<T>> getChunks(int itemsPerChunk) {
List<List<T>> chunks = [];
final numChunks = (items.length / itemsPerChunk).ceil();
for (int i = 0; i < numChunks; i++) {
final index = i * itemsPerChunk;
int endIndex = index + itemsPerChunk;
if (endIndex > items.length) {
endIndex = items.length;
}
chunks.add(items.sublist(index, endIndex));
}
return chunks;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final itemsPerRow =
layout == FlexLayout.grid ? _getItemsPerRow(width) : 1;
final chunks = getChunks(itemsPerRow);
return Column(
children: [
for (final c in chunks) ...[
if (chunks.indexOf(c) > 0) SizedBox(height: runSpacing),
Row(
children: [
for (final entry in c) ...[
Flexible(
child: itemBuilder(entry),
),
if (itemsPerRow != 1 && c.indexOf(entry) != c.length - 1)
SizedBox(width: spacing),
],
if (c.length < itemsPerRow) ...[
// Prevents resizing when an item is removed
SizedBox(width: 8 * (itemsPerRow - c.length).toDouble()),
Spacer(
flex: itemsPerRow - c.length,
)
]
],
),
]
],
);
},
);
}
}

View File

@ -102,7 +102,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
...widget.actions
],
),
onPopInvoked: (didPop) {
onPopInvokedWithResult: (didPop, _) {
if (didPop) {
widget.onCancel?.call();
}

View File

@ -17,6 +17,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class Toast extends StatefulWidget {
final String message;
@ -149,5 +150,7 @@ void Function() showToast(
Overlay.of(context).insert(entry!);
});
SemanticsService.announce(message, TextDirection.ltr);
return close;
}

View File

@ -63,4 +63,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

View File

@ -1,7 +1,7 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
// Keep app running if window closes

12
macos_assemble.patch Normal file
View File

@ -0,0 +1,12 @@
diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh
index 40c6a5051f..a7f05d9113 100755
--- a/packages/flutter_tools/bin/macos_assemble.sh
+++ b/packages/flutter_tools/bin/macos_assemble.sh
@@ -222,6 +222,7 @@ EmbedFrameworks() {
# Iterate through all .frameworks in native assets directory.
for native_asset in "${native_assets_path}"*.framework; do
+ [ -e "$native_asset" ] || continue # Skip when there are no matches.
# Codesign the framework inside the app bundle.
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/$(basename "$native_asset")"
done

View File

@ -5,18 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev"
source: hosted
version: "67.0.0"
version: "72.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: "direct dev"
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev"
source: hosted
version: "6.4.1"
version: "6.7.0"
analyzer_plugin:
dependency: "direct dev"
description:
@ -101,18 +106,18 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
url: "https://pub.dev"
source: hosted
version: "2.4.11"
version: "2.4.12"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
url: "https://pub.dev"
source: hosted
version: "7.3.1"
version: "7.3.2"
built_collection:
dependency: transitive
description:
@ -197,18 +202,18 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+1"
version: "0.3.4+2"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.4"
custom_lint:
dependency: "direct dev"
description:
@ -261,10 +266,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
file:
dependency: transitive
description:
@ -277,10 +282,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258"
sha256: "1375f8685ca6f0412effecc2db834757e9d0e3e055468053e563794b0755cdcd"
url: "https://pub.dev"
source: hosted
version: "8.0.6"
version: "8.1.1"
fixnum:
dependency: transitive
description:
@ -316,10 +321,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
url: "https://pub.dev"
source: hosted
version: "2.0.20"
version: "2.0.21"
flutter_riverpod:
dependency: "direct main"
description:
@ -342,18 +347,18 @@ packages:
dependency: "direct dev"
description:
name: freezed
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.7"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@ -379,10 +384,10 @@ packages:
dependency: transitive
description:
name: graphs
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
hotreloader:
dependency: transitive
description:
@ -395,10 +400,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
http_multi_server:
dependency: transitive
description:
@ -464,18 +469,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
@ -515,6 +520,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
@ -527,18 +540,18 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: a2c78726048c755f0f90fd2b7c8799cd94338e2e9b7ab6498ae56503262c14bc
sha256: "8f4abdb6bc714526ccf66e825b7391d7ca65239484ad92be71980fe73a57521c"
url: "https://pub.dev"
source: hosted
version: "4.2762.0"
version: "4.2780.0"
menu_base:
dependency: transitive
description:
@ -551,10 +564,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.15.0"
mime:
dependency: transitive
description:
@ -591,18 +604,18 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
url: "https://pub.dev"
source: hosted
version: "2.2.6"
version: "2.2.10"
path_provider_foundation:
dependency: transitive
description:
@ -631,10 +644,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
petitparser:
dependency: transitive
description:
@ -647,10 +660,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
@ -726,58 +739,58 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.1"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.1"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.5.2"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
shelf:
dependency: transitive
description:
@ -899,10 +912,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
version: "0.7.2"
test_res:
dependency: "direct dev"
description:
@ -946,26 +959,26 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
url: "https://pub.dev"
source: hosted
version: "6.3.3"
version: "6.3.9"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.2.0"
url_launcher_macos:
dependency: transitive
description:
@ -986,26 +999,26 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
version: "4.4.2"
vector_graphics:
dependency: "direct main"
description:
@ -1042,10 +1055,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "14.2.4"
watcher:
dependency: transitive
description:
@ -1058,26 +1071,26 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
webdriver:
dependency: transitive
description:
@ -1090,10 +1103,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
url: "https://pub.dev"
source: hosted
version: "5.5.1"
version: "5.5.4"
window_manager:
dependency: "direct main"
description:
@ -1128,5 +1141,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.4.3 <4.0.0"
dart: ">=3.5.0-259.0.dev <4.0.0"
flutter: ">=3.22.0"

View File

@ -70,7 +70,7 @@ dependencies:
io: ^1.0.4
base32: ^2.1.3
convert: ^3.1.1
material_symbols_icons: ^4.2719.3
material_symbols_icons: ^4.2741.0
dev_dependencies:
integration_test: