This commit is contained in:
Dain Nilsson 2024-08-13 13:52:24 +02:00
commit 4ac29959e2
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
31 changed files with 471 additions and 174 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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)

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

@ -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)

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

@ -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"
}

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,
@ -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)

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()

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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),

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

@ -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