mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2025-01-08 20:08:45 +03:00
Merge PR #1594
This commit is contained in:
commit
4ac29959e2
13
.github/workflows/android.yml
vendored
13
.github/workflows/android.yml
vendored
@ -7,12 +7,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: set up JDK 17
|
||||
- name: Clone yubikit-android
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Yubico/yubikit-android
|
||||
ref: dain/scp
|
||||
path: kit
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Build yubikit-android
|
||||
run: ./gradlew --stacktrace build publishToMavenLocal
|
||||
working-directory: ./kit
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: 'app'
|
||||
|
11
.github/workflows/codeql-analysis.yml
vendored
11
.github/workflows/codeql-analysis.yml
vendored
@ -33,12 +33,23 @@ jobs:
|
||||
languages: ${{ matrix.language }}
|
||||
setup-python-dependencies: false
|
||||
|
||||
- name: Clone yubikit-android
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Yubico/yubikit-android
|
||||
ref: dain/scp
|
||||
path: kit
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Build yubikit-android
|
||||
run: ./gradlew --stacktrace build publishToMavenLocal
|
||||
working-directory: ./kit
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: 'app'
|
||||
|
@ -48,12 +48,20 @@ import com.yubico.authenticator.oath.AppLinkMethodChannel
|
||||
import com.yubico.authenticator.oath.OathManager
|
||||
import com.yubico.authenticator.oath.OathViewModel
|
||||
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
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
||||
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
|
||||
@ -281,6 +289,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)) {
|
||||
@ -427,7 +454,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()
|
||||
}
|
||||
@ -493,6 +520,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"hasCamera" -> {
|
||||
val cameraService =
|
||||
getSystemService(CAMERA_SERVICE) as CameraManager
|
||||
@ -503,9 +531,11 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
"hasNfc" -> result.success(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
|
||||
)
|
||||
|
||||
"isNfcEnabled" -> {
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(this@MainActivity)
|
||||
|
||||
@ -513,6 +543,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
nfcAdapter != null && nfcAdapter.isEnabled
|
||||
)
|
||||
}
|
||||
|
||||
"openNfcSettings" -> {
|
||||
startActivity(Intent(ACTION_NFC_SETTINGS))
|
||||
result.success(true)
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -53,6 +53,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
|
||||
@ -609,8 +610,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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
project.ext {
|
||||
@ -9,7 +10,7 @@ allprojects {
|
||||
targetSdkVersion = 34
|
||||
compileSdkVersion = 34
|
||||
|
||||
yubiKitVersion = "2.6.0"
|
||||
yubiKitVersion = "2.6.1-SNAPSHOT"
|
||||
junitVersion = "4.13.2"
|
||||
mockitoVersion = "5.12.0"
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .base import (
|
||||
RpcResponse,
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
@ -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):
|
||||
@ -224,7 +225,7 @@ 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)
|
||||
|
||||
|
@ -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"])
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) =>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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(
|
||||
|
@ -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';
|
||||
@ -82,6 +83,9 @@ 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 &&
|
||||
@ -142,37 +146,40 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
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),
|
||||
|
@ -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>),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user