diff --git a/helper/helper/device.py b/helper/helper/device.py index d47df9df..a3a336d8 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -24,6 +24,7 @@ from .oath import OathNode from .fido import Ctap2Node from .yubiotp import YubiOtpNode from .management import ManagementNode +from .piv import PivNode from .qr import scan_qr from ykman import __version__ as ykman_version from ykman.base import PID @@ -391,6 +392,13 @@ class ConnectionNode(RpcNode): def oath(self): return OathNode(self._connection) + @child( + condition=lambda self: isinstance(self._connection, SmartCardConnection) + and CAPABILITY.PIV in self.capabilities + ) + def piv(self): + return PivNode(self._connection) + @child( condition=lambda self: isinstance(self._connection, FidoConnection) and CAPABILITY.FIDO2 in self.capabilities diff --git a/helper/helper/piv.py b/helper/helper/piv.py new file mode 100644 index 00000000..df01dab7 --- /dev/null +++ b/helper/helper/piv.py @@ -0,0 +1,456 @@ +# Copyright (C) 2023 Yubico. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .base import ( + RpcNode, + action, + child, + RpcException, + ChildResetException, + TimeoutException, + AuthRequiredException, +) +from yubikit.core import NotSupportedError, BadResponseError +from yubikit.core.smartcard import ApduError, SW +from yubikit.piv import ( + PivSession, + OBJECT_ID, + MANAGEMENT_KEY_TYPE, + InvalidPinError, + SLOT, + require_version, + KEY_TYPE, + PIN_POLICY, + TOUCH_POLICY, +) +from ykman.piv import ( + get_pivman_data, + get_pivman_protected_data, + derive_management_key, + pivman_set_mgm_key, + pivman_change_pin, + generate_self_signed_certificate, + generate_csr, + generate_chuid, +) +from ykman.util import ( + parse_certificates, + parse_private_key, + get_leaf_certificates, + InvalidPasswordError, +) +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from cryptography.hazmat.primitives import hashes +from dataclasses import asdict +from enum import Enum, unique +from threading import Timer +from time import time +import datetime +import logging + +logger = logging.getLogger(__name__) + +_date_format = "%Y-%m-%d" + + +class InvalidPinException(RpcException): + def __init__(self, cause): + super().__init__( + "invalid-pin", + "Wrong PIN", + dict(attempts_remaining=cause.attempts_remaining), + ) + + +@unique +class GENERATE_TYPE(str, Enum): + CSR = "csr" + CERTIFICATE = "certificate" + + +class PivNode(RpcNode): + def __init__(self, connection): + super().__init__() + self.session = PivSession(connection) + self._pivman_data = get_pivman_data(self.session) + self._authenticated = False + + def __call__(self, *args, **kwargs): + try: + return super().__call__(*args, **kwargs) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + raise AuthRequiredException() + # TODO: This should probably be in a baseclass of all "AppNodes". + raise ChildResetException(f"SW: {e.sw:x}") + except InvalidPinError as e: + raise InvalidPinException(cause=e) + + def _get_object(self, object_id): + try: + return self.session.get_object(object_id) + except ApduError as e: + if e.sw == SW.FILE_NOT_FOUND: + return None + raise + except BadResponseError: + logger.warning(f"Couldn't read data object {object_id}", exc_info=True) + return None + + def get_data(self): + try: + pin_md = self.session.get_pin_metadata() + puk_md = self.session.get_puk_metadata() + mgm_md = self.session.get_management_key_metadata() + pin_attempts = pin_md.attempts_remaining + metadata = dict( + pin_metadata=asdict(pin_md), + puk_metadata=asdict(puk_md), + management_key_metadata=asdict(mgm_md), + ) + except NotSupportedError: + pin_attempts = self.session.get_pin_attempts() + metadata = None + + return dict( + version=self.session.version, + authenticated=self._authenticated, + derived_key=self._pivman_data.has_derived_key, + stored_key=self._pivman_data.has_stored_key, + chuid=self._get_object(OBJECT_ID.CHUID), + ccc=self._get_object(OBJECT_ID.CAPABILITY), + pin_attempts=pin_attempts, + metadata=metadata, + ) + + def _authenticate(self, key, signal): + try: + metadata = self.session.get_management_key_metadata() + key_type = metadata.key_type + if metadata.touch_policy != TOUCH_POLICY.NEVER: + signal("touch") + timer = None + except NotSupportedError: + key_type = MANAGEMENT_KEY_TYPE.TDES + timer = Timer(0.5, lambda: signal("touch")) + timer.start() + try: + # TODO: Check if this is needed, maybe SW is enough + start = time() + self.session.authenticate(key_type, key) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5: + raise TimeoutException() + raise + finally: + if timer: + timer.cancel() + self._authenticated = True + + @action + def verify_pin(self, params, event, signal): + pin = params.pop("pin") + + self.session.verify_pin(pin) + key = None + + if self._pivman_data.has_derived_key: + key = derive_management_key(pin, self._pivman_data.salt) + elif self._pivman_data.has_stored_key: + pivman_prot = get_pivman_protected_data(self.session) + key = pivman_prot.key + if key: + try: + self._authenticate(key, signal) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + pass # Authenticate failed, bad derived key? + + # Ensure verify was the last thing we did + self.session.verify_pin(pin) + + return dict(status=True, authenticated=self._authenticated) + + @action + def authenticate(self, params, event, signal): + key = bytes.fromhex(params.pop("key")) + try: + self._authenticate(key, signal) + return dict(status=True) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + return dict(status=False) + raise + + @action(condition=lambda self: self._authenticated) + def set_key(self, params, event, signal): + key_type = MANAGEMENT_KEY_TYPE(params.pop("key_type", MANAGEMENT_KEY_TYPE.TDES)) + key = bytes.fromhex(params.pop("key")) + 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() + + @action + def change_pin(self, params, event, signal): + old_pin = params.pop("pin") + new_pin = params.pop("new_pin") + pivman_change_pin(self.session, old_pin, new_pin) + return dict() + + @action + def change_puk(self, params, event, signal): + old_puk = params.pop("puk") + new_puk = params.pop("new_puk") + self.session.change_puk(old_puk, new_puk) + return dict() + + @action + def unblock_pin(self, params, event, signal): + puk = params.pop("puk") + new_pin = params.pop("new_pin") + self.session.unblock_pin(puk, new_pin) + 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() + + @child + def slots(self): + return SlotsNode(self.session) + + +def _slot_for(name): + return SLOT(int(name, base=16)) + + +def _parse_file(data, password=None): + if password: + password = password.encode() + try: + certs = parse_certificates(data, password) + except (ValueError, TypeError): + certs = [] + + try: + private_key = parse_private_key(data, password) + except (ValueError, TypeError): + private_key = None + + return private_key, certs + + +class SlotsNode(RpcNode): + def __init__(self, session): + super().__init__() + self.session = session + try: + require_version(session.version, (5, 3, 0)) + self._has_metadata = True + except NotSupportedError: + self._has_metadata = False + self.refresh() + + def refresh(self): + self._slots = {} + for slot in set(SLOT) - {SLOT.ATTESTATION}: + metadata = None + if self._has_metadata: + try: + metadata = self.session.get_slot_metadata(slot) + except (ApduError, BadResponseError): + pass + try: + certificate = self.session.get_certificate(slot) + except (ApduError, BadResponseError): + # TODO: Differentiate between none and malformed + certificate = None + self._slots[slot] = (metadata, certificate) + if self._child and _slot_for(self._child_name) not in self._slots: + self._close_child() + + def list_children(self): + return { + f"{int(slot):02x}": dict( + slot=int(slot), + name=slot.name, + has_key=metadata is not None if self._has_metadata else None, + cert_info=dict( + subject=cert.subject.rfc4514_string(), + issuer=cert.issuer.rfc4514_string(), + serial=hex(cert.serial_number)[2:], + not_valid_before=cert.not_valid_before.isoformat(), + not_valid_after=cert.not_valid_after.isoformat(), + fingerprint=cert.fingerprint(hashes.SHA256()), + ) + if cert + else None, + ) + for slot, (metadata, cert) in self._slots.items() + } + + def create_child(self, name): + slot = _slot_for(name) + if slot in self._slots: + metadata, certificate = self._slots[slot] + return SlotNode(self.session, slot, metadata, certificate, self.refresh) + return super().create_child(name) + + @action + def examine_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + try: + private_key, certs = _parse_file(data, password) + return dict( + status=True, + password=password is not None, + private_key=bool(private_key), + certificates=len(certs), + ) + except InvalidPasswordError: + return dict(status=False) + + +class SlotNode(RpcNode): + def __init__(self, session, slot, metadata, certificate, refresh): + super().__init__() + self.session = session + self.slot = slot + self.metadata = metadata + self.certificate = certificate + self._refresh = refresh + + def get_data(self): + return dict( + id=f"{int(self.slot):02x}", + name=self.slot.name, + metadata=asdict(self.metadata) if self.metadata else None, + certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode() + if self.certificate + else None, + ) + + @action(condition=lambda self: self.certificate) + def delete(self, params, event, signal): + self.session.delete_certificate(self.slot) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self._refresh() + self.certificate = None + return dict() + + @action + def import_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + + try: + private_key, certs = _parse_file(data, password) + except InvalidPasswordError: + logger.debug("InvalidPassword", exc_info=True) + raise ValueError("Wrong/Missing password") + + # Exception? + if not certs and not private_key: + raise ValueError("Failed to parse") + + metadata = None + if private_key: + pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT)) + touch_policy = TOUCH_POLICY( + params.pop("touch_policy", TOUCH_POLICY.DEFAULT) + ) + self.session.put_key(self.slot, private_key, pin_policy, touch_policy) + try: + metadata = self.session.get_slot_metadata(self.slot) + except (ApduError, BadResponseError): + pass + + if certs: + if len(certs) > 1: + leafs = get_leaf_certificates(certs) + certificate = leafs[0] + else: + certificate = certs[0] + self.session.put_certificate(self.slot, certificate) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self.certificate = certificate + + self._refresh() + + return dict( + metadata=asdict(metadata) if metadata 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 + def generate(self, params, event, signal): + key_type = KEY_TYPE(params.pop("key_type")) + pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT)) + touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT)) + subject = params.pop("subject") + generate_type = GENERATE_TYPE(params.pop("generate_type", GENERATE_TYPE.CERTIFICATE)) + public_key = self.session.generate_key( + self.slot, key_type, pin_policy, touch_policy + ) + + if pin_policy != PIN_POLICY.NEVER: + # TODO: Check if verified? + pin = params.pop("pin") + self.session.verify_pin(pin) + + if touch_policy in (TOUCH_POLICY.ALWAYS, TOUCH_POLICY.CACHED): + signal("touch") + + if generate_type == GENERATE_TYPE.CSR: + result = generate_csr(self.session, self.slot, public_key, subject) + elif generate_type == GENERATE_TYPE.CERTIFICATE: + now = datetime.datetime.utcnow() + then = now + datetime.timedelta(days=365) + valid_from = params.pop("valid_from", now.strftime(_date_format)) + valid_to = params.pop("valid_to", then.strftime(_date_format)) + result = generate_self_signed_certificate( + self.session, + self.slot, + public_key, + subject, + datetime.datetime.strptime(valid_from, _date_format), + datetime.datetime.strptime(valid_to, _date_format), + ) + self.session.put_certificate(self.slot, result) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + else: + raise ValueError("Unsupported GENERATE_TYPE") + + self._refresh() + + return dict( + public_key=public_key.public_bytes( + encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo + ).decode(), + result=result.public_bytes(encoding=Encoding.PEM).decode(), + ) diff --git a/lib/app/models.dart b/lib/app/models.dart index 22c4875f..78cec6be 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -53,6 +53,7 @@ enum Application { String getDisplayName(AppLocalizations l10n) => switch (this) { Application.oath => l10n.s_authenticator, Application.fido => l10n.s_webauthn, + Application.piv => "PIV", //TODO _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 229272c3..46d87bbb 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -26,6 +26,7 @@ import '../../fido/views/fido_screen.dart'; import '../../oath/models.dart'; import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; +import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; import '../message.dart'; import '../models.dart'; @@ -161,6 +162,7 @@ class MainPage extends ConsumerWidget { return switch (app) { Application.oath => OathScreen(data.node.path), Application.fido => FidoScreen(data), + Application.piv => PivScreen(data.node.path), _ => MessagePage( header: l10n.s_app_not_supported, message: l10n.l_app_not_supported_desc, diff --git a/lib/core/models.dart b/lib/core/models.dart index eee2b3c6..4187431f 100644 --- a/lib/core/models.dart +++ b/lib/core/models.dart @@ -16,6 +16,7 @@ import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import '../management/models.dart'; @@ -152,3 +153,5 @@ class Version with _$Version implements Comparable { return a - b; } } + +final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 04885c0b..76f74492 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -41,11 +41,13 @@ import '../core/state.dart'; import '../fido/state.dart'; import '../management/state.dart'; import '../oath/state.dart'; +import '../piv/state.dart'; import '../version.dart'; import 'devices.dart'; import 'fido/state.dart'; import 'management/state.dart'; import 'oath/state.dart'; +import 'piv/state.dart'; import 'qr_scanner.dart'; import 'rpc.dart'; import 'state.dart'; @@ -177,6 +179,7 @@ Future initialize(List argv) async { supportedAppsProvider.overrideWithValue([ Application.oath, Application.fido, + Application.piv, Application.management, ]), prefProvider.overrideWithValue(prefs), @@ -184,6 +187,12 @@ Future initialize(List argv) async { windowStateProvider.overrideWith( (ref) => ref.watch(desktopWindowStateProvider), ), + clipboardProvider.overrideWith( + (ref) => ref.watch(desktopClipboardProvider), + ), + supportedThemesProvider.overrideWith( + (ref) => ref.watch(desktopSupportedThemesProvider), + ), attachedDevicesProvider.overrideWith( () => DesktopDevicesNotifier(), ), @@ -206,12 +215,9 @@ Future initialize(List argv) async { fidoStateProvider.overrideWithProvider(desktopFidoState), fingerprintProvider.overrideWithProvider(desktopFingerprintProvider), credentialProvider.overrideWithProvider(desktopCredentialProvider), - clipboardProvider.overrideWith( - (ref) => ref.watch(desktopClipboardProvider), - ), - supportedThemesProvider.overrideWith( - (ref) => ref.watch(desktopSupportedThemesProvider), - ) + // PIV + pivStateProvider.overrideWithProvider(desktopPivState), + pivSlotsProvider.overrideWithProvider(desktopPivSlots), ], child: YubicoAuthenticatorApp( page: Consumer( diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart new file mode 100644 index 00000000..27c9a961 --- /dev/null +++ b/lib/desktop/piv/state.dart @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/desktop/models.dart'; + +import '../../app/logging.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../app/views/user_interaction.dart'; +import '../../core/models.dart'; +import '../../piv/models.dart'; +import '../../piv/state.dart'; +import '../rpc.dart'; +import '../state.dart'; + +final _log = Logger('desktop.piv.state'); + +final _managementKeyProvider = + StateProvider.autoDispose.family( + (ref, _) => null, +); + +final _pinProvider = StateProvider.autoDispose.family( + (ref, _) => null, +); + +final _sessionProvider = + Provider.autoDispose.family( + (ref, devicePath) { + // Make sure the managementKey and PIN are held for the duration of the session. + ref.watch(_managementKeyProvider(devicePath)); + ref.watch(_pinProvider(devicePath)); + return RpcNodeSession( + ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'piv']); + }, +); + +final desktopPivState = AsyncNotifierProvider.autoDispose + .family( + _DesktopPivStateNotifier.new); + +class _DesktopPivStateNotifier extends PivStateNotifier { + late RpcNodeSession _session; + late DevicePath _devicePath; + + @override + FutureOr build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + _session + ..setErrorHandler('state-reset', (_) async { + ref.invalidate(_sessionProvider(devicePath)); + }) + ..setErrorHandler('auth-required', (_) async { + final String? mgmtKey; + if (state.valueOrNull?.metadata?.managementKeyMetadata.defaultValue == + true) { + mgmtKey = defaultManagementKey; + } else { + mgmtKey = ref.read(_managementKeyProvider(devicePath)); + } + if (mgmtKey != null) { + if (await authenticate(mgmtKey)) { + ref.invalidateSelf(); + } else { + ref.read(_managementKeyProvider(devicePath).notifier).state = null; + } + } + }); + ref.onDispose(() { + _session + ..unsetErrorHandler('state-reset') + ..unsetErrorHandler('auth-required'); + }); + _devicePath = devicePath; + + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + final pivState = PivState.fromJson(result['data']); + + return pivState; + } + + @override + Future reset() async { + await _session.command('reset'); + ref.invalidate(_sessionProvider(_session.devicePath)); + } + + @override + Future authenticate(String managementKey) async { + final withContext = ref.watch(withContextProvider); + + final signaler = Signaler(); + UserInteractionController? controller; + try { + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + + final result = await _session.command( + 'authenticate', + params: {'key': managementKey}, + signal: signaler, + ); + + if (result['status']) { + ref.read(_managementKeyProvider(_devicePath).notifier).state = + managementKey; + final oldState = state.valueOrNull; + if (oldState != null) { + state = AsyncData(oldState.copyWith(authenticated: true)); + } + return true; + } else { + return false; + } + } finally { + controller?.close(); + } + } + + @override + Future verifyPin(String pin) async { + final pivState = state.valueOrNull; + + final signaler = Signaler(); + UserInteractionController? controller; + try { + if (pivState?.protectedKey == true) { + // Might require touch as this will also authenticate + final withContext = ref.watch(withContextProvider); + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + } + await _session.command( + 'verify_pin', + params: {'pin': pin}, + signal: signaler, + ); + + ref.read(_pinProvider(_devicePath).notifier).state = pin; + + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + controller?.close(); + ref.invalidateSelf(); + } + } + + @override + Future changePin(String pin, String newPin) async { + try { + await _session.command( + 'change_pin', + params: {'pin': pin, 'new_pin': newPin}, + ); + ref.read(_pinProvider(_devicePath).notifier).state = null; + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } + + @override + Future changePuk(String puk, String newPuk) async { + try { + await _session.command( + 'change_puk', + params: {'puk': puk, 'new_puk': newPuk}, + ); + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } + + @override + Future setManagementKey(String managementKey, + {ManagementKeyType managementKeyType = defaultManagementKeyType, + bool storeKey = false}) async { + await _session.command( + 'set_key', + params: { + 'key': managementKey, + 'key_type': managementKeyType.value, + 'store_key': storeKey, + }, + ); + ref.invalidateSelf(); + } + + @override + Future unblockPin(String puk, String newPin) async { + try { + await _session.command( + 'unblock_pin', + params: {'puk': puk, 'new_pin': newPin}, + ); + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } +} + +final _shownSlots = SlotId.values.map((slot) => slot.id).toList(); + +extension on SlotId { + String get node => id.toRadixString(16).padLeft(2, '0'); +} + +final desktopPivSlots = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopPivSlotsNotifier.new); + +class _DesktopPivSlotsNotifier extends PivSlotsNotifier { + late RpcNodeSession _session; + + @override + FutureOr> build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + + final result = await _session.command('get', target: ['slots']); + return (result['children'] as Map) + .values + .where((e) => _shownSlots.contains(e['slot'])) + .map((e) => PivSlot.fromJson(e)) + .toList(); + } + + @override + Future delete(SlotId slot) async { + await _session.command('delete', target: ['slots', slot.node]); + ref.invalidateSelf(); + } + + @override + Future generate( + SlotId slot, + KeyType keyType, { + required PivGenerateParameters parameters, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + String? pin, + }) async { + final withContext = ref.watch(withContextProvider); + + final signaler = Signaler(); + UserInteractionController? controller; + try { + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + + final (type, subject, validFrom, validTo) = parameters.when( + certificate: (subject, validFrom, validTo) => ( + GenerateType.certificate, + subject, + dateFormatter.format(validFrom), + dateFormatter.format(validTo), + ), + csr: (subject) => ( + GenerateType.csr, + subject, + null, + null, + ), + ); + + final pin = ref.read(_pinProvider(_session.devicePath)); + + final result = await _session.command( + 'generate', + target: [ + 'slots', + slot.node, + ], + params: { + 'key_type': keyType.value, + 'pin_policy': pinPolicy.value, + 'touch_policy': touchPolicy.value, + 'subject': subject, + 'generate_type': type.name, + 'valid_from': validFrom, + 'valid_to': validTo, + 'pin': pin, + }, + signal: signaler, + ); + + ref.invalidateSelf(); + + return PivGenerateResult.fromJson( + {'generate_type': type.name, ...result}); + } finally { + controller?.close(); + } + } + + @override + Future examine(String data, {String? password}) async { + final result = await _session.command('examine_file', target: [ + 'slots', + ], params: { + 'data': data, + 'password': password, + }); + + if (result['status']) { + return PivExamineResult.fromJson({'runtimeType': 'result', ...result}); + } else { + return PivExamineResult.invalidPassword(); + } + } + + @override + Future import(SlotId slot, String data, + {String? password, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault}) async { + final result = await _session.command('import_file', target: [ + 'slots', + slot.node, + ], params: { + 'data': data, + 'password': password, + 'pin_policy': pinPolicy.value, + 'touch_policy': touchPolicy.value, + }); + + ref.invalidateSelf(); + return PivImportResult.fromJson(result); + } + + @override + Future<(SlotMetadata?, String?)> read(SlotId slot) async { + final result = await _session.command('get', target: [ + 'slots', + slot.node, + ]); + final data = result['data']; + final metadata = data['metadata']; + return ( + metadata != null ? SlotMetadata.fromJson(metadata) : null, + data['certificate'] as String?, + ); + } +} diff --git a/lib/oath/views/actions.dart b/lib/oath/views/actions.dart index d10868b3..cea57615 100755 --- a/lib/oath/views/actions.dart +++ b/lib/oath/views/actions.dart @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart new file mode 100644 index 00000000..3ee1b6ee --- /dev/null +++ b/lib/piv/keys.dart @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022-2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +const _prefix = 'piv.keys'; + +const managePinAction = Key('$_prefix.manage_pin'); +const managePukAction = Key('$_prefix.manage_puk'); +const manageManagementKeyAction = Key('$_prefix.manage_management_key'); +const resetAction = Key('$_prefix.reset'); + +const setupMacOsAction = Key('$_prefix.setup_macos'); +const saveButton = Key('$_prefix.save'); +const deleteButton = Key('$_prefix.delete'); +const unlockButton = Key('$_prefix.unlock'); + +const managementKeyField = Key('$_prefix.management_key'); +const pinPukField = Key('$_prefix.pin_puk'); +const newPinPukField = Key('$_prefix.new_pin_puk'); +const confirmPinPukField = Key('$_prefix.confirm_pin_puk'); +const subjectField = Key('$_prefix.subject'); diff --git a/lib/piv/models.dart b/lib/piv/models.dart new file mode 100644 index 00000000..4a77206a --- /dev/null +++ b/lib/piv/models.dart @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../core/models.dart'; + +part 'models.freezed.dart'; +part 'models.g.dart'; + +const defaultManagementKey = '010203040506070801020304050607080102030405060708'; +const defaultManagementKeyType = ManagementKeyType.tdes; +const defaultKeyType = KeyType.rsa2048; +const defaultGenerateType = GenerateType.certificate; + +enum GenerateType { + certificate, + csr; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +enum SlotId { + authentication(0x9a), + signature(0x9c), + keyManagement(0x9d), + cardAuth(0x9e); + + final int id; + const SlotId(this.id); + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } + + factory SlotId.fromJson(int value) => + SlotId.values.firstWhere((e) => e.id == value); +} + +@JsonEnum(alwaysCreate: true) +enum PinPolicy { + @JsonValue(0x00) + dfault, + @JsonValue(0x01) + never, + @JsonValue(0x02) + once, + @JsonValue(0x03) + always; + + const PinPolicy(); + + int get value => _$PinPolicyEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@JsonEnum(alwaysCreate: true) +enum TouchPolicy { + @JsonValue(0x00) + dfault, + @JsonValue(0x01) + never, + @JsonValue(0x02) + always, + @JsonValue(0x03) + cached; + + const TouchPolicy(); + + int get value => _$TouchPolicyEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@JsonEnum(alwaysCreate: true) +enum KeyType { + @JsonValue(0x06) + rsa1024, + @JsonValue(0x07) + rsa2048, + @JsonValue(0x11) + eccp256, + @JsonValue(0x14) + eccp384; + + const KeyType(); + + int get value => _$KeyTypeEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +enum ManagementKeyType { + @JsonValue(0x03) + tdes, + @JsonValue(0x08) + aes128, + @JsonValue(0x0A) + aes192, + @JsonValue(0x0C) + aes256; + + const ManagementKeyType(); + + int get value => _$ManagementKeyTypeEnumMap[this]!; + + int get keyLength => switch (this) { + ManagementKeyType.tdes => 24, + ManagementKeyType.aes128 => 16, + ManagementKeyType.aes192 => 24, + ManagementKeyType.aes256 => 32, + }; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@freezed +class PinMetadata with _$PinMetadata { + factory PinMetadata( + bool defaultValue, + int totalAttempts, + int attemptsRemaining, + ) = _PinMetadata; + + factory PinMetadata.fromJson(Map json) => + _$PinMetadataFromJson(json); +} + +@freezed +class PinVerificationStatus with _$PinVerificationStatus { + const factory PinVerificationStatus.success() = _PinSuccess; + factory PinVerificationStatus.failure(int attemptsRemaining) = _PinFailure; +} + +@freezed +class ManagementKeyMetadata with _$ManagementKeyMetadata { + factory ManagementKeyMetadata( + ManagementKeyType keyType, + bool defaultValue, + TouchPolicy touchPolicy, + ) = _ManagementKeyMetadata; + + factory ManagementKeyMetadata.fromJson(Map json) => + _$ManagementKeyMetadataFromJson(json); +} + +@freezed +class SlotMetadata with _$SlotMetadata { + factory SlotMetadata( + KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded, + ) = _SlotMetadata; + + factory SlotMetadata.fromJson(Map json) => + _$SlotMetadataFromJson(json); +} + +@freezed +class PivStateMetadata with _$PivStateMetadata { + factory PivStateMetadata({ + required ManagementKeyMetadata managementKeyMetadata, + required PinMetadata pinMetadata, + required PinMetadata pukMetadata, + }) = _PivStateMetadata; + + factory PivStateMetadata.fromJson(Map json) => + _$PivStateMetadataFromJson(json); +} + +@freezed +class PivState with _$PivState { + const PivState._(); + + factory PivState({ + required Version version, + required bool authenticated, + required bool derivedKey, + required bool storedKey, + required int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata, + }) = _PivState; + + bool get protectedKey => derivedKey || storedKey; + bool get needsAuth => + !authenticated && metadata?.managementKeyMetadata.defaultValue != true; + + factory PivState.fromJson(Map json) => + _$PivStateFromJson(json); +} + +@freezed +class CertInfo with _$CertInfo { + factory CertInfo({ + required String subject, + required String issuer, + required String serial, + required String notValidBefore, + required String notValidAfter, + required String fingerprint, + }) = _CertInfo; + + factory CertInfo.fromJson(Map json) => + _$CertInfoFromJson(json); +} + +@freezed +class PivSlot with _$PivSlot { + factory PivSlot({ + required SlotId slot, + bool? hasKey, + CertInfo? certInfo, + }) = _PivSlot; + + factory PivSlot.fromJson(Map json) => + _$PivSlotFromJson(json); +} + +@freezed +class PivExamineResult with _$PivExamineResult { + factory PivExamineResult.result({ + required bool password, + required bool privateKey, + required int certificates, + }) = _ExamineResult; + factory PivExamineResult.invalidPassword() = _InvalidPassword; + + factory PivExamineResult.fromJson(Map json) => + _$PivExamineResultFromJson(json); +} + +@freezed +class PivGenerateParameters with _$PivGenerateParameters { + factory PivGenerateParameters.certificate({ + required String subject, + required DateTime validFrom, + required DateTime validTo, + }) = _GenerateCertificate; + factory PivGenerateParameters.csr({ + required String subject, + }) = _GenerateCsr; +} + +@freezed +class PivGenerateResult with _$PivGenerateResult { + factory PivGenerateResult({ + required GenerateType generateType, + required String publicKey, + required String result, + }) = _PivGenerateResult; + + factory PivGenerateResult.fromJson(Map json) => + _$PivGenerateResultFromJson(json); +} + +@freezed +class PivImportResult with _$PivImportResult { + factory PivImportResult({ + required SlotMetadata? metadata, + required String? publicKey, + required String? certificate, + }) = _PivImportResult; + + factory PivImportResult.fromJson(Map json) => + _$PivImportResultFromJson(json); +} diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart new file mode 100644 index 00000000..8a1637f5 --- /dev/null +++ b/lib/piv/models.freezed.dart @@ -0,0 +1,2984 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +PinMetadata _$PinMetadataFromJson(Map json) { + return _PinMetadata.fromJson(json); +} + +/// @nodoc +mixin _$PinMetadata { + bool get defaultValue => throw _privateConstructorUsedError; + int get totalAttempts => throw _privateConstructorUsedError; + int get attemptsRemaining => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PinMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PinMetadataCopyWith<$Res> { + factory $PinMetadataCopyWith( + PinMetadata value, $Res Function(PinMetadata) then) = + _$PinMetadataCopyWithImpl<$Res, PinMetadata>; + @useResult + $Res call({bool defaultValue, int totalAttempts, int attemptsRemaining}); +} + +/// @nodoc +class _$PinMetadataCopyWithImpl<$Res, $Val extends PinMetadata> + implements $PinMetadataCopyWith<$Res> { + _$PinMetadataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? defaultValue = null, + Object? totalAttempts = null, + Object? attemptsRemaining = null, + }) { + return _then(_value.copyWith( + defaultValue: null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + totalAttempts: null == totalAttempts + ? _value.totalAttempts + : totalAttempts // ignore: cast_nullable_to_non_nullable + as int, + attemptsRemaining: null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_PinMetadataCopyWith<$Res> + implements $PinMetadataCopyWith<$Res> { + factory _$$_PinMetadataCopyWith( + _$_PinMetadata value, $Res Function(_$_PinMetadata) then) = + __$$_PinMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool defaultValue, int totalAttempts, int attemptsRemaining}); +} + +/// @nodoc +class __$$_PinMetadataCopyWithImpl<$Res> + extends _$PinMetadataCopyWithImpl<$Res, _$_PinMetadata> + implements _$$_PinMetadataCopyWith<$Res> { + __$$_PinMetadataCopyWithImpl( + _$_PinMetadata _value, $Res Function(_$_PinMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? defaultValue = null, + Object? totalAttempts = null, + Object? attemptsRemaining = null, + }) { + return _then(_$_PinMetadata( + null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + null == totalAttempts + ? _value.totalAttempts + : totalAttempts // ignore: cast_nullable_to_non_nullable + as int, + null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PinMetadata implements _PinMetadata { + _$_PinMetadata(this.defaultValue, this.totalAttempts, this.attemptsRemaining); + + factory _$_PinMetadata.fromJson(Map json) => + _$$_PinMetadataFromJson(json); + + @override + final bool defaultValue; + @override + final int totalAttempts; + @override + final int attemptsRemaining; + + @override + String toString() { + return 'PinMetadata(defaultValue: $defaultValue, totalAttempts: $totalAttempts, attemptsRemaining: $attemptsRemaining)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PinMetadata && + (identical(other.defaultValue, defaultValue) || + other.defaultValue == defaultValue) && + (identical(other.totalAttempts, totalAttempts) || + other.totalAttempts == totalAttempts) && + (identical(other.attemptsRemaining, attemptsRemaining) || + other.attemptsRemaining == attemptsRemaining)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, defaultValue, totalAttempts, attemptsRemaining); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PinMetadataCopyWith<_$_PinMetadata> get copyWith => + __$$_PinMetadataCopyWithImpl<_$_PinMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_PinMetadataToJson( + this, + ); + } +} + +abstract class _PinMetadata implements PinMetadata { + factory _PinMetadata(final bool defaultValue, final int totalAttempts, + final int attemptsRemaining) = _$_PinMetadata; + + factory _PinMetadata.fromJson(Map json) = + _$_PinMetadata.fromJson; + + @override + bool get defaultValue; + @override + int get totalAttempts; + @override + int get attemptsRemaining; + @override + @JsonKey(ignore: true) + _$$_PinMetadataCopyWith<_$_PinMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PinVerificationStatus { + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PinVerificationStatusCopyWith<$Res> { + factory $PinVerificationStatusCopyWith(PinVerificationStatus value, + $Res Function(PinVerificationStatus) then) = + _$PinVerificationStatusCopyWithImpl<$Res, PinVerificationStatus>; +} + +/// @nodoc +class _$PinVerificationStatusCopyWithImpl<$Res, + $Val extends PinVerificationStatus> + implements $PinVerificationStatusCopyWith<$Res> { + _$PinVerificationStatusCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_PinSuccessCopyWith<$Res> { + factory _$$_PinSuccessCopyWith( + _$_PinSuccess value, $Res Function(_$_PinSuccess) then) = + __$$_PinSuccessCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_PinSuccessCopyWithImpl<$Res> + extends _$PinVerificationStatusCopyWithImpl<$Res, _$_PinSuccess> + implements _$$_PinSuccessCopyWith<$Res> { + __$$_PinSuccessCopyWithImpl( + _$_PinSuccess _value, $Res Function(_$_PinSuccess) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_PinSuccess implements _PinSuccess { + const _$_PinSuccess(); + + @override + String toString() { + return 'PinVerificationStatus.success()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_PinSuccess); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _PinSuccess implements PinVerificationStatus { + const factory _PinSuccess() = _$_PinSuccess; +} + +/// @nodoc +abstract class _$$_PinFailureCopyWith<$Res> { + factory _$$_PinFailureCopyWith( + _$_PinFailure value, $Res Function(_$_PinFailure) then) = + __$$_PinFailureCopyWithImpl<$Res>; + @useResult + $Res call({int attemptsRemaining}); +} + +/// @nodoc +class __$$_PinFailureCopyWithImpl<$Res> + extends _$PinVerificationStatusCopyWithImpl<$Res, _$_PinFailure> + implements _$$_PinFailureCopyWith<$Res> { + __$$_PinFailureCopyWithImpl( + _$_PinFailure _value, $Res Function(_$_PinFailure) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attemptsRemaining = null, + }) { + return _then(_$_PinFailure( + null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$_PinFailure implements _PinFailure { + _$_PinFailure(this.attemptsRemaining); + + @override + final int attemptsRemaining; + + @override + String toString() { + return 'PinVerificationStatus.failure(attemptsRemaining: $attemptsRemaining)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PinFailure && + (identical(other.attemptsRemaining, attemptsRemaining) || + other.attemptsRemaining == attemptsRemaining)); + } + + @override + int get hashCode => Object.hash(runtimeType, attemptsRemaining); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PinFailureCopyWith<_$_PinFailure> get copyWith => + __$$_PinFailureCopyWithImpl<_$_PinFailure>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) { + return failure(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) { + return failure?.call(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(attemptsRemaining); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _PinFailure implements PinVerificationStatus { + factory _PinFailure(final int attemptsRemaining) = _$_PinFailure; + + int get attemptsRemaining; + @JsonKey(ignore: true) + _$$_PinFailureCopyWith<_$_PinFailure> get copyWith => + throw _privateConstructorUsedError; +} + +ManagementKeyMetadata _$ManagementKeyMetadataFromJson( + Map json) { + return _ManagementKeyMetadata.fromJson(json); +} + +/// @nodoc +mixin _$ManagementKeyMetadata { + ManagementKeyType get keyType => throw _privateConstructorUsedError; + bool get defaultValue => throw _privateConstructorUsedError; + TouchPolicy get touchPolicy => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ManagementKeyMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ManagementKeyMetadataCopyWith<$Res> { + factory $ManagementKeyMetadataCopyWith(ManagementKeyMetadata value, + $Res Function(ManagementKeyMetadata) then) = + _$ManagementKeyMetadataCopyWithImpl<$Res, ManagementKeyMetadata>; + @useResult + $Res call( + {ManagementKeyType keyType, bool defaultValue, TouchPolicy touchPolicy}); +} + +/// @nodoc +class _$ManagementKeyMetadataCopyWithImpl<$Res, + $Val extends ManagementKeyMetadata> + implements $ManagementKeyMetadataCopyWith<$Res> { + _$ManagementKeyMetadataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? defaultValue = null, + Object? touchPolicy = null, + }) { + return _then(_value.copyWith( + keyType: null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as ManagementKeyType, + defaultValue: null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + touchPolicy: null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_ManagementKeyMetadataCopyWith<$Res> + implements $ManagementKeyMetadataCopyWith<$Res> { + factory _$$_ManagementKeyMetadataCopyWith(_$_ManagementKeyMetadata value, + $Res Function(_$_ManagementKeyMetadata) then) = + __$$_ManagementKeyMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ManagementKeyType keyType, bool defaultValue, TouchPolicy touchPolicy}); +} + +/// @nodoc +class __$$_ManagementKeyMetadataCopyWithImpl<$Res> + extends _$ManagementKeyMetadataCopyWithImpl<$Res, _$_ManagementKeyMetadata> + implements _$$_ManagementKeyMetadataCopyWith<$Res> { + __$$_ManagementKeyMetadataCopyWithImpl(_$_ManagementKeyMetadata _value, + $Res Function(_$_ManagementKeyMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? defaultValue = null, + Object? touchPolicy = null, + }) { + return _then(_$_ManagementKeyMetadata( + null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as ManagementKeyType, + null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ManagementKeyMetadata implements _ManagementKeyMetadata { + _$_ManagementKeyMetadata(this.keyType, this.defaultValue, this.touchPolicy); + + factory _$_ManagementKeyMetadata.fromJson(Map json) => + _$$_ManagementKeyMetadataFromJson(json); + + @override + final ManagementKeyType keyType; + @override + final bool defaultValue; + @override + final TouchPolicy touchPolicy; + + @override + String toString() { + return 'ManagementKeyMetadata(keyType: $keyType, defaultValue: $defaultValue, touchPolicy: $touchPolicy)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ManagementKeyMetadata && + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.defaultValue, defaultValue) || + other.defaultValue == defaultValue) && + (identical(other.touchPolicy, touchPolicy) || + other.touchPolicy == touchPolicy)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, keyType, defaultValue, touchPolicy); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ManagementKeyMetadataCopyWith<_$_ManagementKeyMetadata> get copyWith => + __$$_ManagementKeyMetadataCopyWithImpl<_$_ManagementKeyMetadata>( + this, _$identity); + + @override + Map toJson() { + return _$$_ManagementKeyMetadataToJson( + this, + ); + } +} + +abstract class _ManagementKeyMetadata implements ManagementKeyMetadata { + factory _ManagementKeyMetadata( + final ManagementKeyType keyType, + final bool defaultValue, + final TouchPolicy touchPolicy) = _$_ManagementKeyMetadata; + + factory _ManagementKeyMetadata.fromJson(Map json) = + _$_ManagementKeyMetadata.fromJson; + + @override + ManagementKeyType get keyType; + @override + bool get defaultValue; + @override + TouchPolicy get touchPolicy; + @override + @JsonKey(ignore: true) + _$$_ManagementKeyMetadataCopyWith<_$_ManagementKeyMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +SlotMetadata _$SlotMetadataFromJson(Map json) { + return _SlotMetadata.fromJson(json); +} + +/// @nodoc +mixin _$SlotMetadata { + KeyType get keyType => throw _privateConstructorUsedError; + PinPolicy get pinPolicy => throw _privateConstructorUsedError; + TouchPolicy get touchPolicy => throw _privateConstructorUsedError; + bool get generated => throw _privateConstructorUsedError; + String get publicKeyEncoded => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SlotMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SlotMetadataCopyWith<$Res> { + factory $SlotMetadataCopyWith( + SlotMetadata value, $Res Function(SlotMetadata) then) = + _$SlotMetadataCopyWithImpl<$Res, SlotMetadata>; + @useResult + $Res call( + {KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded}); +} + +/// @nodoc +class _$SlotMetadataCopyWithImpl<$Res, $Val extends SlotMetadata> + implements $SlotMetadataCopyWith<$Res> { + _$SlotMetadataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? pinPolicy = null, + Object? touchPolicy = null, + Object? generated = null, + Object? publicKeyEncoded = null, + }) { + return _then(_value.copyWith( + keyType: null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType, + pinPolicy: null == pinPolicy + ? _value.pinPolicy + : pinPolicy // ignore: cast_nullable_to_non_nullable + as PinPolicy, + touchPolicy: null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + generated: null == generated + ? _value.generated + : generated // ignore: cast_nullable_to_non_nullable + as bool, + publicKeyEncoded: null == publicKeyEncoded + ? _value.publicKeyEncoded + : publicKeyEncoded // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_SlotMetadataCopyWith<$Res> + implements $SlotMetadataCopyWith<$Res> { + factory _$$_SlotMetadataCopyWith( + _$_SlotMetadata value, $Res Function(_$_SlotMetadata) then) = + __$$_SlotMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded}); +} + +/// @nodoc +class __$$_SlotMetadataCopyWithImpl<$Res> + extends _$SlotMetadataCopyWithImpl<$Res, _$_SlotMetadata> + implements _$$_SlotMetadataCopyWith<$Res> { + __$$_SlotMetadataCopyWithImpl( + _$_SlotMetadata _value, $Res Function(_$_SlotMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? pinPolicy = null, + Object? touchPolicy = null, + Object? generated = null, + Object? publicKeyEncoded = null, + }) { + return _then(_$_SlotMetadata( + null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType, + null == pinPolicy + ? _value.pinPolicy + : pinPolicy // ignore: cast_nullable_to_non_nullable + as PinPolicy, + null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + null == generated + ? _value.generated + : generated // ignore: cast_nullable_to_non_nullable + as bool, + null == publicKeyEncoded + ? _value.publicKeyEncoded + : publicKeyEncoded // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_SlotMetadata implements _SlotMetadata { + _$_SlotMetadata(this.keyType, this.pinPolicy, this.touchPolicy, + this.generated, this.publicKeyEncoded); + + factory _$_SlotMetadata.fromJson(Map json) => + _$$_SlotMetadataFromJson(json); + + @override + final KeyType keyType; + @override + final PinPolicy pinPolicy; + @override + final TouchPolicy touchPolicy; + @override + final bool generated; + @override + final String publicKeyEncoded; + + @override + String toString() { + return 'SlotMetadata(keyType: $keyType, pinPolicy: $pinPolicy, touchPolicy: $touchPolicy, generated: $generated, publicKeyEncoded: $publicKeyEncoded)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotMetadata && + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.pinPolicy, pinPolicy) || + other.pinPolicy == pinPolicy) && + (identical(other.touchPolicy, touchPolicy) || + other.touchPolicy == touchPolicy) && + (identical(other.generated, generated) || + other.generated == generated) && + (identical(other.publicKeyEncoded, publicKeyEncoded) || + other.publicKeyEncoded == publicKeyEncoded)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, keyType, pinPolicy, touchPolicy, + generated, publicKeyEncoded); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotMetadataCopyWith<_$_SlotMetadata> get copyWith => + __$$_SlotMetadataCopyWithImpl<_$_SlotMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_SlotMetadataToJson( + this, + ); + } +} + +abstract class _SlotMetadata implements SlotMetadata { + factory _SlotMetadata( + final KeyType keyType, + final PinPolicy pinPolicy, + final TouchPolicy touchPolicy, + final bool generated, + final String publicKeyEncoded) = _$_SlotMetadata; + + factory _SlotMetadata.fromJson(Map json) = + _$_SlotMetadata.fromJson; + + @override + KeyType get keyType; + @override + PinPolicy get pinPolicy; + @override + TouchPolicy get touchPolicy; + @override + bool get generated; + @override + String get publicKeyEncoded; + @override + @JsonKey(ignore: true) + _$$_SlotMetadataCopyWith<_$_SlotMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +PivStateMetadata _$PivStateMetadataFromJson(Map json) { + return _PivStateMetadata.fromJson(json); +} + +/// @nodoc +mixin _$PivStateMetadata { + ManagementKeyMetadata get managementKeyMetadata => + throw _privateConstructorUsedError; + PinMetadata get pinMetadata => throw _privateConstructorUsedError; + PinMetadata get pukMetadata => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivStateMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivStateMetadataCopyWith<$Res> { + factory $PivStateMetadataCopyWith( + PivStateMetadata value, $Res Function(PivStateMetadata) then) = + _$PivStateMetadataCopyWithImpl<$Res, PivStateMetadata>; + @useResult + $Res call( + {ManagementKeyMetadata managementKeyMetadata, + PinMetadata pinMetadata, + PinMetadata pukMetadata}); + + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata; + $PinMetadataCopyWith<$Res> get pinMetadata; + $PinMetadataCopyWith<$Res> get pukMetadata; +} + +/// @nodoc +class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> + implements $PivStateMetadataCopyWith<$Res> { + _$PivStateMetadataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? managementKeyMetadata = null, + Object? pinMetadata = null, + Object? pukMetadata = null, + }) { + return _then(_value.copyWith( + managementKeyMetadata: null == managementKeyMetadata + ? _value.managementKeyMetadata + : managementKeyMetadata // ignore: cast_nullable_to_non_nullable + as ManagementKeyMetadata, + pinMetadata: null == pinMetadata + ? _value.pinMetadata + : pinMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + pukMetadata: null == pukMetadata + ? _value.pukMetadata + : pukMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata { + return $ManagementKeyMetadataCopyWith<$Res>(_value.managementKeyMetadata, + (value) { + return _then(_value.copyWith(managementKeyMetadata: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PinMetadataCopyWith<$Res> get pinMetadata { + return $PinMetadataCopyWith<$Res>(_value.pinMetadata, (value) { + return _then(_value.copyWith(pinMetadata: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PinMetadataCopyWith<$Res> get pukMetadata { + return $PinMetadataCopyWith<$Res>(_value.pukMetadata, (value) { + return _then(_value.copyWith(pukMetadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivStateMetadataCopyWith<$Res> + implements $PivStateMetadataCopyWith<$Res> { + factory _$$_PivStateMetadataCopyWith( + _$_PivStateMetadata value, $Res Function(_$_PivStateMetadata) then) = + __$$_PivStateMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ManagementKeyMetadata managementKeyMetadata, + PinMetadata pinMetadata, + PinMetadata pukMetadata}); + + @override + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata; + @override + $PinMetadataCopyWith<$Res> get pinMetadata; + @override + $PinMetadataCopyWith<$Res> get pukMetadata; +} + +/// @nodoc +class __$$_PivStateMetadataCopyWithImpl<$Res> + extends _$PivStateMetadataCopyWithImpl<$Res, _$_PivStateMetadata> + implements _$$_PivStateMetadataCopyWith<$Res> { + __$$_PivStateMetadataCopyWithImpl( + _$_PivStateMetadata _value, $Res Function(_$_PivStateMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? managementKeyMetadata = null, + Object? pinMetadata = null, + Object? pukMetadata = null, + }) { + return _then(_$_PivStateMetadata( + managementKeyMetadata: null == managementKeyMetadata + ? _value.managementKeyMetadata + : managementKeyMetadata // ignore: cast_nullable_to_non_nullable + as ManagementKeyMetadata, + pinMetadata: null == pinMetadata + ? _value.pinMetadata + : pinMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + pukMetadata: null == pukMetadata + ? _value.pukMetadata + : pukMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivStateMetadata implements _PivStateMetadata { + _$_PivStateMetadata( + {required this.managementKeyMetadata, + required this.pinMetadata, + required this.pukMetadata}); + + factory _$_PivStateMetadata.fromJson(Map json) => + _$$_PivStateMetadataFromJson(json); + + @override + final ManagementKeyMetadata managementKeyMetadata; + @override + final PinMetadata pinMetadata; + @override + final PinMetadata pukMetadata; + + @override + String toString() { + return 'PivStateMetadata(managementKeyMetadata: $managementKeyMetadata, pinMetadata: $pinMetadata, pukMetadata: $pukMetadata)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivStateMetadata && + (identical(other.managementKeyMetadata, managementKeyMetadata) || + other.managementKeyMetadata == managementKeyMetadata) && + (identical(other.pinMetadata, pinMetadata) || + other.pinMetadata == pinMetadata) && + (identical(other.pukMetadata, pukMetadata) || + other.pukMetadata == pukMetadata)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, managementKeyMetadata, pinMetadata, pukMetadata); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivStateMetadataCopyWith<_$_PivStateMetadata> get copyWith => + __$$_PivStateMetadataCopyWithImpl<_$_PivStateMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_PivStateMetadataToJson( + this, + ); + } +} + +abstract class _PivStateMetadata implements PivStateMetadata { + factory _PivStateMetadata( + {required final ManagementKeyMetadata managementKeyMetadata, + required final PinMetadata pinMetadata, + required final PinMetadata pukMetadata}) = _$_PivStateMetadata; + + factory _PivStateMetadata.fromJson(Map json) = + _$_PivStateMetadata.fromJson; + + @override + ManagementKeyMetadata get managementKeyMetadata; + @override + PinMetadata get pinMetadata; + @override + PinMetadata get pukMetadata; + @override + @JsonKey(ignore: true) + _$$_PivStateMetadataCopyWith<_$_PivStateMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +PivState _$PivStateFromJson(Map json) { + return _PivState.fromJson(json); +} + +/// @nodoc +mixin _$PivState { + Version get version => throw _privateConstructorUsedError; + bool get authenticated => throw _privateConstructorUsedError; + bool get derivedKey => throw _privateConstructorUsedError; + bool get storedKey => throw _privateConstructorUsedError; + int get pinAttempts => throw _privateConstructorUsedError; + String? get chuid => throw _privateConstructorUsedError; + String? get ccc => throw _privateConstructorUsedError; + PivStateMetadata? get metadata => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivStateCopyWith<$Res> { + factory $PivStateCopyWith(PivState value, $Res Function(PivState) then) = + _$PivStateCopyWithImpl<$Res, PivState>; + @useResult + $Res call( + {Version version, + bool authenticated, + bool derivedKey, + bool storedKey, + int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata}); + + $VersionCopyWith<$Res> get version; + $PivStateMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class _$PivStateCopyWithImpl<$Res, $Val extends PivState> + implements $PivStateCopyWith<$Res> { + _$PivStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? version = null, + Object? authenticated = null, + Object? derivedKey = null, + Object? storedKey = null, + Object? pinAttempts = null, + Object? chuid = freezed, + Object? ccc = freezed, + Object? metadata = freezed, + }) { + return _then(_value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as Version, + authenticated: null == authenticated + ? _value.authenticated + : authenticated // ignore: cast_nullable_to_non_nullable + as bool, + derivedKey: null == derivedKey + ? _value.derivedKey + : derivedKey // ignore: cast_nullable_to_non_nullable + as bool, + storedKey: null == storedKey + ? _value.storedKey + : storedKey // ignore: cast_nullable_to_non_nullable + as bool, + pinAttempts: null == pinAttempts + ? _value.pinAttempts + : pinAttempts // ignore: cast_nullable_to_non_nullable + as int, + chuid: freezed == chuid + ? _value.chuid + : chuid // ignore: cast_nullable_to_non_nullable + as String?, + ccc: freezed == ccc + ? _value.ccc + : ccc // ignore: cast_nullable_to_non_nullable + as String?, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as PivStateMetadata?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $VersionCopyWith<$Res> get version { + return $VersionCopyWith<$Res>(_value.version, (value) { + return _then(_value.copyWith(version: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PivStateMetadataCopyWith<$Res>? get metadata { + if (_value.metadata == null) { + return null; + } + + return $PivStateMetadataCopyWith<$Res>(_value.metadata!, (value) { + return _then(_value.copyWith(metadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivStateCopyWith<$Res> implements $PivStateCopyWith<$Res> { + factory _$$_PivStateCopyWith( + _$_PivState value, $Res Function(_$_PivState) then) = + __$$_PivStateCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Version version, + bool authenticated, + bool derivedKey, + bool storedKey, + int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata}); + + @override + $VersionCopyWith<$Res> get version; + @override + $PivStateMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class __$$_PivStateCopyWithImpl<$Res> + extends _$PivStateCopyWithImpl<$Res, _$_PivState> + implements _$$_PivStateCopyWith<$Res> { + __$$_PivStateCopyWithImpl( + _$_PivState _value, $Res Function(_$_PivState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? version = null, + Object? authenticated = null, + Object? derivedKey = null, + Object? storedKey = null, + Object? pinAttempts = null, + Object? chuid = freezed, + Object? ccc = freezed, + Object? metadata = freezed, + }) { + return _then(_$_PivState( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as Version, + authenticated: null == authenticated + ? _value.authenticated + : authenticated // ignore: cast_nullable_to_non_nullable + as bool, + derivedKey: null == derivedKey + ? _value.derivedKey + : derivedKey // ignore: cast_nullable_to_non_nullable + as bool, + storedKey: null == storedKey + ? _value.storedKey + : storedKey // ignore: cast_nullable_to_non_nullable + as bool, + pinAttempts: null == pinAttempts + ? _value.pinAttempts + : pinAttempts // ignore: cast_nullable_to_non_nullable + as int, + chuid: freezed == chuid + ? _value.chuid + : chuid // ignore: cast_nullable_to_non_nullable + as String?, + ccc: freezed == ccc + ? _value.ccc + : ccc // ignore: cast_nullable_to_non_nullable + as String?, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as PivStateMetadata?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivState extends _PivState { + _$_PivState( + {required this.version, + required this.authenticated, + required this.derivedKey, + required this.storedKey, + required this.pinAttempts, + this.chuid, + this.ccc, + this.metadata}) + : super._(); + + factory _$_PivState.fromJson(Map json) => + _$$_PivStateFromJson(json); + + @override + final Version version; + @override + final bool authenticated; + @override + final bool derivedKey; + @override + final bool storedKey; + @override + final int pinAttempts; + @override + final String? chuid; + @override + final String? ccc; + @override + final PivStateMetadata? metadata; + + @override + String toString() { + return 'PivState(version: $version, authenticated: $authenticated, derivedKey: $derivedKey, storedKey: $storedKey, pinAttempts: $pinAttempts, chuid: $chuid, ccc: $ccc, metadata: $metadata)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivState && + (identical(other.version, version) || other.version == version) && + (identical(other.authenticated, authenticated) || + other.authenticated == authenticated) && + (identical(other.derivedKey, derivedKey) || + other.derivedKey == derivedKey) && + (identical(other.storedKey, storedKey) || + other.storedKey == storedKey) && + (identical(other.pinAttempts, pinAttempts) || + other.pinAttempts == pinAttempts) && + (identical(other.chuid, chuid) || other.chuid == chuid) && + (identical(other.ccc, ccc) || other.ccc == ccc) && + (identical(other.metadata, metadata) || + other.metadata == metadata)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, version, authenticated, + derivedKey, storedKey, pinAttempts, chuid, ccc, metadata); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivStateCopyWith<_$_PivState> get copyWith => + __$$_PivStateCopyWithImpl<_$_PivState>(this, _$identity); + + @override + Map toJson() { + return _$$_PivStateToJson( + this, + ); + } +} + +abstract class _PivState extends PivState { + factory _PivState( + {required final Version version, + required final bool authenticated, + required final bool derivedKey, + required final bool storedKey, + required final int pinAttempts, + final String? chuid, + final String? ccc, + final PivStateMetadata? metadata}) = _$_PivState; + _PivState._() : super._(); + + factory _PivState.fromJson(Map json) = _$_PivState.fromJson; + + @override + Version get version; + @override + bool get authenticated; + @override + bool get derivedKey; + @override + bool get storedKey; + @override + int get pinAttempts; + @override + String? get chuid; + @override + String? get ccc; + @override + PivStateMetadata? get metadata; + @override + @JsonKey(ignore: true) + _$$_PivStateCopyWith<_$_PivState> get copyWith => + throw _privateConstructorUsedError; +} + +CertInfo _$CertInfoFromJson(Map json) { + return _CertInfo.fromJson(json); +} + +/// @nodoc +mixin _$CertInfo { + String get subject => throw _privateConstructorUsedError; + String get issuer => throw _privateConstructorUsedError; + String get serial => throw _privateConstructorUsedError; + String get notValidBefore => throw _privateConstructorUsedError; + String get notValidAfter => throw _privateConstructorUsedError; + String get fingerprint => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CertInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CertInfoCopyWith<$Res> { + factory $CertInfoCopyWith(CertInfo value, $Res Function(CertInfo) then) = + _$CertInfoCopyWithImpl<$Res, CertInfo>; + @useResult + $Res call( + {String subject, + String issuer, + String serial, + String notValidBefore, + String notValidAfter, + String fingerprint}); +} + +/// @nodoc +class _$CertInfoCopyWithImpl<$Res, $Val extends CertInfo> + implements $CertInfoCopyWith<$Res> { + _$CertInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + Object? issuer = null, + Object? serial = null, + Object? notValidBefore = null, + Object? notValidAfter = null, + Object? fingerprint = null, + }) { + return _then(_value.copyWith( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + issuer: null == issuer + ? _value.issuer + : issuer // ignore: cast_nullable_to_non_nullable + as String, + serial: null == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String, + notValidBefore: null == notValidBefore + ? _value.notValidBefore + : notValidBefore // ignore: cast_nullable_to_non_nullable + as String, + notValidAfter: null == notValidAfter + ? _value.notValidAfter + : notValidAfter // ignore: cast_nullable_to_non_nullable + as String, + fingerprint: null == fingerprint + ? _value.fingerprint + : fingerprint // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CertInfoCopyWith<$Res> implements $CertInfoCopyWith<$Res> { + factory _$$_CertInfoCopyWith( + _$_CertInfo value, $Res Function(_$_CertInfo) then) = + __$$_CertInfoCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String subject, + String issuer, + String serial, + String notValidBefore, + String notValidAfter, + String fingerprint}); +} + +/// @nodoc +class __$$_CertInfoCopyWithImpl<$Res> + extends _$CertInfoCopyWithImpl<$Res, _$_CertInfo> + implements _$$_CertInfoCopyWith<$Res> { + __$$_CertInfoCopyWithImpl( + _$_CertInfo _value, $Res Function(_$_CertInfo) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + Object? issuer = null, + Object? serial = null, + Object? notValidBefore = null, + Object? notValidAfter = null, + Object? fingerprint = null, + }) { + return _then(_$_CertInfo( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + issuer: null == issuer + ? _value.issuer + : issuer // ignore: cast_nullable_to_non_nullable + as String, + serial: null == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String, + notValidBefore: null == notValidBefore + ? _value.notValidBefore + : notValidBefore // ignore: cast_nullable_to_non_nullable + as String, + notValidAfter: null == notValidAfter + ? _value.notValidAfter + : notValidAfter // ignore: cast_nullable_to_non_nullable + as String, + fingerprint: null == fingerprint + ? _value.fingerprint + : fingerprint // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_CertInfo implements _CertInfo { + _$_CertInfo( + {required this.subject, + required this.issuer, + required this.serial, + required this.notValidBefore, + required this.notValidAfter, + required this.fingerprint}); + + factory _$_CertInfo.fromJson(Map json) => + _$$_CertInfoFromJson(json); + + @override + final String subject; + @override + final String issuer; + @override + final String serial; + @override + final String notValidBefore; + @override + final String notValidAfter; + @override + final String fingerprint; + + @override + String toString() { + return 'CertInfo(subject: $subject, issuer: $issuer, serial: $serial, notValidBefore: $notValidBefore, notValidAfter: $notValidAfter, fingerprint: $fingerprint)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CertInfo && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.issuer, issuer) || other.issuer == issuer) && + (identical(other.serial, serial) || other.serial == serial) && + (identical(other.notValidBefore, notValidBefore) || + other.notValidBefore == notValidBefore) && + (identical(other.notValidAfter, notValidAfter) || + other.notValidAfter == notValidAfter) && + (identical(other.fingerprint, fingerprint) || + other.fingerprint == fingerprint)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, subject, issuer, serial, + notValidBefore, notValidAfter, fingerprint); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CertInfoCopyWith<_$_CertInfo> get copyWith => + __$$_CertInfoCopyWithImpl<_$_CertInfo>(this, _$identity); + + @override + Map toJson() { + return _$$_CertInfoToJson( + this, + ); + } +} + +abstract class _CertInfo implements CertInfo { + factory _CertInfo( + {required final String subject, + required final String issuer, + required final String serial, + required final String notValidBefore, + required final String notValidAfter, + required final String fingerprint}) = _$_CertInfo; + + factory _CertInfo.fromJson(Map json) = _$_CertInfo.fromJson; + + @override + String get subject; + @override + String get issuer; + @override + String get serial; + @override + String get notValidBefore; + @override + String get notValidAfter; + @override + String get fingerprint; + @override + @JsonKey(ignore: true) + _$$_CertInfoCopyWith<_$_CertInfo> get copyWith => + throw _privateConstructorUsedError; +} + +PivSlot _$PivSlotFromJson(Map json) { + return _PivSlot.fromJson(json); +} + +/// @nodoc +mixin _$PivSlot { + SlotId get slot => throw _privateConstructorUsedError; + bool? get hasKey => throw _privateConstructorUsedError; + CertInfo? get certInfo => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivSlotCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivSlotCopyWith<$Res> { + factory $PivSlotCopyWith(PivSlot value, $Res Function(PivSlot) then) = + _$PivSlotCopyWithImpl<$Res, PivSlot>; + @useResult + $Res call({SlotId slot, bool? hasKey, CertInfo? certInfo}); + + $CertInfoCopyWith<$Res>? get certInfo; +} + +/// @nodoc +class _$PivSlotCopyWithImpl<$Res, $Val extends PivSlot> + implements $PivSlotCopyWith<$Res> { + _$PivSlotCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot = null, + Object? hasKey = freezed, + Object? certInfo = freezed, + }) { + return _then(_value.copyWith( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + hasKey: freezed == hasKey + ? _value.hasKey + : hasKey // ignore: cast_nullable_to_non_nullable + as bool?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $CertInfoCopyWith<$Res>? get certInfo { + if (_value.certInfo == null) { + return null; + } + + return $CertInfoCopyWith<$Res>(_value.certInfo!, (value) { + return _then(_value.copyWith(certInfo: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivSlotCopyWith<$Res> implements $PivSlotCopyWith<$Res> { + factory _$$_PivSlotCopyWith( + _$_PivSlot value, $Res Function(_$_PivSlot) then) = + __$$_PivSlotCopyWithImpl<$Res>; + @override + @useResult + $Res call({SlotId slot, bool? hasKey, CertInfo? certInfo}); + + @override + $CertInfoCopyWith<$Res>? get certInfo; +} + +/// @nodoc +class __$$_PivSlotCopyWithImpl<$Res> + extends _$PivSlotCopyWithImpl<$Res, _$_PivSlot> + implements _$$_PivSlotCopyWith<$Res> { + __$$_PivSlotCopyWithImpl(_$_PivSlot _value, $Res Function(_$_PivSlot) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot = null, + Object? hasKey = freezed, + Object? certInfo = freezed, + }) { + return _then(_$_PivSlot( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + hasKey: freezed == hasKey + ? _value.hasKey + : hasKey // ignore: cast_nullable_to_non_nullable + as bool?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivSlot implements _PivSlot { + _$_PivSlot({required this.slot, this.hasKey, this.certInfo}); + + factory _$_PivSlot.fromJson(Map json) => + _$$_PivSlotFromJson(json); + + @override + final SlotId slot; + @override + final bool? hasKey; + @override + final CertInfo? certInfo; + + @override + String toString() { + return 'PivSlot(slot: $slot, hasKey: $hasKey, certInfo: $certInfo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivSlot && + (identical(other.slot, slot) || other.slot == slot) && + (identical(other.hasKey, hasKey) || other.hasKey == hasKey) && + (identical(other.certInfo, certInfo) || + other.certInfo == certInfo)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, slot, hasKey, certInfo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivSlotCopyWith<_$_PivSlot> get copyWith => + __$$_PivSlotCopyWithImpl<_$_PivSlot>(this, _$identity); + + @override + Map toJson() { + return _$$_PivSlotToJson( + this, + ); + } +} + +abstract class _PivSlot implements PivSlot { + factory _PivSlot( + {required final SlotId slot, + final bool? hasKey, + final CertInfo? certInfo}) = _$_PivSlot; + + factory _PivSlot.fromJson(Map json) = _$_PivSlot.fromJson; + + @override + SlotId get slot; + @override + bool? get hasKey; + @override + CertInfo? get certInfo; + @override + @JsonKey(ignore: true) + _$$_PivSlotCopyWith<_$_PivSlot> get copyWith => + throw _privateConstructorUsedError; +} + +PivExamineResult _$PivExamineResultFromJson(Map json) { + switch (json['runtimeType']) { + case 'result': + return _ExamineResult.fromJson(json); + case 'invalidPassword': + return _InvalidPassword.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'PivExamineResult', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$PivExamineResult { + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivExamineResultCopyWith<$Res> { + factory $PivExamineResultCopyWith( + PivExamineResult value, $Res Function(PivExamineResult) then) = + _$PivExamineResultCopyWithImpl<$Res, PivExamineResult>; +} + +/// @nodoc +class _$PivExamineResultCopyWithImpl<$Res, $Val extends PivExamineResult> + implements $PivExamineResultCopyWith<$Res> { + _$PivExamineResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_ExamineResultCopyWith<$Res> { + factory _$$_ExamineResultCopyWith( + _$_ExamineResult value, $Res Function(_$_ExamineResult) then) = + __$$_ExamineResultCopyWithImpl<$Res>; + @useResult + $Res call({bool password, bool privateKey, int certificates}); +} + +/// @nodoc +class __$$_ExamineResultCopyWithImpl<$Res> + extends _$PivExamineResultCopyWithImpl<$Res, _$_ExamineResult> + implements _$$_ExamineResultCopyWith<$Res> { + __$$_ExamineResultCopyWithImpl( + _$_ExamineResult _value, $Res Function(_$_ExamineResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? password = null, + Object? privateKey = null, + Object? certificates = null, + }) { + return _then(_$_ExamineResult( + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as bool, + privateKey: null == privateKey + ? _value.privateKey + : privateKey // ignore: cast_nullable_to_non_nullable + as bool, + certificates: null == certificates + ? _value.certificates + : certificates // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ExamineResult implements _ExamineResult { + _$_ExamineResult( + {required this.password, + required this.privateKey, + required this.certificates, + final String? $type}) + : $type = $type ?? 'result'; + + factory _$_ExamineResult.fromJson(Map json) => + _$$_ExamineResultFromJson(json); + + @override + final bool password; + @override + final bool privateKey; + @override + final int certificates; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PivExamineResult.result(password: $password, privateKey: $privateKey, certificates: $certificates)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ExamineResult && + (identical(other.password, password) || + other.password == password) && + (identical(other.privateKey, privateKey) || + other.privateKey == privateKey) && + (identical(other.certificates, certificates) || + other.certificates == certificates)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, password, privateKey, certificates); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith => + __$$_ExamineResultCopyWithImpl<_$_ExamineResult>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) { + return result(password, privateKey, certificates); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) { + return result?.call(password, privateKey, certificates); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) { + if (result != null) { + return result(password, privateKey, certificates); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) { + return result(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) { + return result?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) { + if (result != null) { + return result(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_ExamineResultToJson( + this, + ); + } +} + +abstract class _ExamineResult implements PivExamineResult { + factory _ExamineResult( + {required final bool password, + required final bool privateKey, + required final int certificates}) = _$_ExamineResult; + + factory _ExamineResult.fromJson(Map json) = + _$_ExamineResult.fromJson; + + bool get password; + bool get privateKey; + int get certificates; + @JsonKey(ignore: true) + _$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_InvalidPasswordCopyWith<$Res> { + factory _$$_InvalidPasswordCopyWith( + _$_InvalidPassword value, $Res Function(_$_InvalidPassword) then) = + __$$_InvalidPasswordCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_InvalidPasswordCopyWithImpl<$Res> + extends _$PivExamineResultCopyWithImpl<$Res, _$_InvalidPassword> + implements _$$_InvalidPasswordCopyWith<$Res> { + __$$_InvalidPasswordCopyWithImpl( + _$_InvalidPassword _value, $Res Function(_$_InvalidPassword) _then) + : super(_value, _then); +} + +/// @nodoc +@JsonSerializable() +class _$_InvalidPassword implements _InvalidPassword { + _$_InvalidPassword({final String? $type}) + : $type = $type ?? 'invalidPassword'; + + factory _$_InvalidPassword.fromJson(Map json) => + _$$_InvalidPasswordFromJson(json); + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PivExamineResult.invalidPassword()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_InvalidPassword); + } + + @JsonKey(ignore: true) + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) { + return invalidPassword(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) { + return invalidPassword?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) { + if (invalidPassword != null) { + return invalidPassword(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) { + return invalidPassword(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) { + return invalidPassword?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) { + if (invalidPassword != null) { + return invalidPassword(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_InvalidPasswordToJson( + this, + ); + } +} + +abstract class _InvalidPassword implements PivExamineResult { + factory _InvalidPassword() = _$_InvalidPassword; + + factory _InvalidPassword.fromJson(Map json) = + _$_InvalidPassword.fromJson; +} + +/// @nodoc +mixin _$PivGenerateParameters { + String get subject => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PivGenerateParametersCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivGenerateParametersCopyWith<$Res> { + factory $PivGenerateParametersCopyWith(PivGenerateParameters value, + $Res Function(PivGenerateParameters) then) = + _$PivGenerateParametersCopyWithImpl<$Res, PivGenerateParameters>; + @useResult + $Res call({String subject}); +} + +/// @nodoc +class _$PivGenerateParametersCopyWithImpl<$Res, + $Val extends PivGenerateParameters> + implements $PivGenerateParametersCopyWith<$Res> { + _$PivGenerateParametersCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + }) { + return _then(_value.copyWith( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_GenerateCertificateCopyWith<$Res> + implements $PivGenerateParametersCopyWith<$Res> { + factory _$$_GenerateCertificateCopyWith(_$_GenerateCertificate value, + $Res Function(_$_GenerateCertificate) then) = + __$$_GenerateCertificateCopyWithImpl<$Res>; + @override + @useResult + $Res call({String subject, DateTime validFrom, DateTime validTo}); +} + +/// @nodoc +class __$$_GenerateCertificateCopyWithImpl<$Res> + extends _$PivGenerateParametersCopyWithImpl<$Res, _$_GenerateCertificate> + implements _$$_GenerateCertificateCopyWith<$Res> { + __$$_GenerateCertificateCopyWithImpl(_$_GenerateCertificate _value, + $Res Function(_$_GenerateCertificate) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + Object? validFrom = null, + Object? validTo = null, + }) { + return _then(_$_GenerateCertificate( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + validFrom: null == validFrom + ? _value.validFrom + : validFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + validTo: null == validTo + ? _value.validTo + : validTo // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$_GenerateCertificate implements _GenerateCertificate { + _$_GenerateCertificate( + {required this.subject, required this.validFrom, required this.validTo}); + + @override + final String subject; + @override + final DateTime validFrom; + @override + final DateTime validTo; + + @override + String toString() { + return 'PivGenerateParameters.certificate(subject: $subject, validFrom: $validFrom, validTo: $validTo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GenerateCertificate && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.validFrom, validFrom) || + other.validFrom == validFrom) && + (identical(other.validTo, validTo) || other.validTo == validTo)); + } + + @override + int get hashCode => Object.hash(runtimeType, subject, validFrom, validTo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GenerateCertificateCopyWith<_$_GenerateCertificate> get copyWith => + __$$_GenerateCertificateCopyWithImpl<_$_GenerateCertificate>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) { + return certificate(subject, validFrom, validTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) { + return certificate?.call(subject, validFrom, validTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) { + if (certificate != null) { + return certificate(subject, validFrom, validTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) { + return certificate(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) { + return certificate?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) { + if (certificate != null) { + return certificate(this); + } + return orElse(); + } +} + +abstract class _GenerateCertificate implements PivGenerateParameters { + factory _GenerateCertificate( + {required final String subject, + required final DateTime validFrom, + required final DateTime validTo}) = _$_GenerateCertificate; + + @override + String get subject; + DateTime get validFrom; + DateTime get validTo; + @override + @JsonKey(ignore: true) + _$$_GenerateCertificateCopyWith<_$_GenerateCertificate> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_GenerateCsrCopyWith<$Res> + implements $PivGenerateParametersCopyWith<$Res> { + factory _$$_GenerateCsrCopyWith( + _$_GenerateCsr value, $Res Function(_$_GenerateCsr) then) = + __$$_GenerateCsrCopyWithImpl<$Res>; + @override + @useResult + $Res call({String subject}); +} + +/// @nodoc +class __$$_GenerateCsrCopyWithImpl<$Res> + extends _$PivGenerateParametersCopyWithImpl<$Res, _$_GenerateCsr> + implements _$$_GenerateCsrCopyWith<$Res> { + __$$_GenerateCsrCopyWithImpl( + _$_GenerateCsr _value, $Res Function(_$_GenerateCsr) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + }) { + return _then(_$_GenerateCsr( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_GenerateCsr implements _GenerateCsr { + _$_GenerateCsr({required this.subject}); + + @override + final String subject; + + @override + String toString() { + return 'PivGenerateParameters.csr(subject: $subject)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GenerateCsr && + (identical(other.subject, subject) || other.subject == subject)); + } + + @override + int get hashCode => Object.hash(runtimeType, subject); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GenerateCsrCopyWith<_$_GenerateCsr> get copyWith => + __$$_GenerateCsrCopyWithImpl<_$_GenerateCsr>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) { + return csr(subject); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) { + return csr?.call(subject); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) { + if (csr != null) { + return csr(subject); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) { + return csr(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) { + return csr?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) { + if (csr != null) { + return csr(this); + } + return orElse(); + } +} + +abstract class _GenerateCsr implements PivGenerateParameters { + factory _GenerateCsr({required final String subject}) = _$_GenerateCsr; + + @override + String get subject; + @override + @JsonKey(ignore: true) + _$$_GenerateCsrCopyWith<_$_GenerateCsr> get copyWith => + throw _privateConstructorUsedError; +} + +PivGenerateResult _$PivGenerateResultFromJson(Map json) { + return _PivGenerateResult.fromJson(json); +} + +/// @nodoc +mixin _$PivGenerateResult { + GenerateType get generateType => throw _privateConstructorUsedError; + String get publicKey => throw _privateConstructorUsedError; + String get result => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivGenerateResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivGenerateResultCopyWith<$Res> { + factory $PivGenerateResultCopyWith( + PivGenerateResult value, $Res Function(PivGenerateResult) then) = + _$PivGenerateResultCopyWithImpl<$Res, PivGenerateResult>; + @useResult + $Res call({GenerateType generateType, String publicKey, String result}); +} + +/// @nodoc +class _$PivGenerateResultCopyWithImpl<$Res, $Val extends PivGenerateResult> + implements $PivGenerateResultCopyWith<$Res> { + _$PivGenerateResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? generateType = null, + Object? publicKey = null, + Object? result = null, + }) { + return _then(_value.copyWith( + generateType: null == generateType + ? _value.generateType + : generateType // ignore: cast_nullable_to_non_nullable + as GenerateType, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + result: null == result + ? _value.result + : result // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_PivGenerateResultCopyWith<$Res> + implements $PivGenerateResultCopyWith<$Res> { + factory _$$_PivGenerateResultCopyWith(_$_PivGenerateResult value, + $Res Function(_$_PivGenerateResult) then) = + __$$_PivGenerateResultCopyWithImpl<$Res>; + @override + @useResult + $Res call({GenerateType generateType, String publicKey, String result}); +} + +/// @nodoc +class __$$_PivGenerateResultCopyWithImpl<$Res> + extends _$PivGenerateResultCopyWithImpl<$Res, _$_PivGenerateResult> + implements _$$_PivGenerateResultCopyWith<$Res> { + __$$_PivGenerateResultCopyWithImpl( + _$_PivGenerateResult _value, $Res Function(_$_PivGenerateResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? generateType = null, + Object? publicKey = null, + Object? result = null, + }) { + return _then(_$_PivGenerateResult( + generateType: null == generateType + ? _value.generateType + : generateType // ignore: cast_nullable_to_non_nullable + as GenerateType, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + result: null == result + ? _value.result + : result // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivGenerateResult implements _PivGenerateResult { + _$_PivGenerateResult( + {required this.generateType, + required this.publicKey, + required this.result}); + + factory _$_PivGenerateResult.fromJson(Map json) => + _$$_PivGenerateResultFromJson(json); + + @override + final GenerateType generateType; + @override + final String publicKey; + @override + final String result; + + @override + String toString() { + return 'PivGenerateResult(generateType: $generateType, publicKey: $publicKey, result: $result)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivGenerateResult && + (identical(other.generateType, generateType) || + other.generateType == generateType) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.result, result) || other.result == result)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, generateType, publicKey, result); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivGenerateResultCopyWith<_$_PivGenerateResult> get copyWith => + __$$_PivGenerateResultCopyWithImpl<_$_PivGenerateResult>( + this, _$identity); + + @override + Map toJson() { + return _$$_PivGenerateResultToJson( + this, + ); + } +} + +abstract class _PivGenerateResult implements PivGenerateResult { + factory _PivGenerateResult( + {required final GenerateType generateType, + required final String publicKey, + required final String result}) = _$_PivGenerateResult; + + factory _PivGenerateResult.fromJson(Map json) = + _$_PivGenerateResult.fromJson; + + @override + GenerateType get generateType; + @override + String get publicKey; + @override + String get result; + @override + @JsonKey(ignore: true) + _$$_PivGenerateResultCopyWith<_$_PivGenerateResult> get copyWith => + throw _privateConstructorUsedError; +} + +PivImportResult _$PivImportResultFromJson(Map json) { + return _PivImportResult.fromJson(json); +} + +/// @nodoc +mixin _$PivImportResult { + SlotMetadata? get metadata => throw _privateConstructorUsedError; + String? get publicKey => throw _privateConstructorUsedError; + String? get certificate => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivImportResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivImportResultCopyWith<$Res> { + factory $PivImportResultCopyWith( + PivImportResult value, $Res Function(PivImportResult) then) = + _$PivImportResultCopyWithImpl<$Res, PivImportResult>; + @useResult + $Res call({SlotMetadata? metadata, String? publicKey, String? certificate}); + + $SlotMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class _$PivImportResultCopyWithImpl<$Res, $Val extends PivImportResult> + implements $PivImportResultCopyWith<$Res> { + _$PivImportResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? metadata = freezed, + Object? publicKey = freezed, + Object? certificate = freezed, + }) { + return _then(_value.copyWith( + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as SlotMetadata?, + publicKey: freezed == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String?, + certificate: freezed == certificate + ? _value.certificate + : certificate // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $SlotMetadataCopyWith<$Res>? get metadata { + if (_value.metadata == null) { + return null; + } + + return $SlotMetadataCopyWith<$Res>(_value.metadata!, (value) { + return _then(_value.copyWith(metadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivImportResultCopyWith<$Res> + implements $PivImportResultCopyWith<$Res> { + factory _$$_PivImportResultCopyWith( + _$_PivImportResult value, $Res Function(_$_PivImportResult) then) = + __$$_PivImportResultCopyWithImpl<$Res>; + @override + @useResult + $Res call({SlotMetadata? metadata, String? publicKey, String? certificate}); + + @override + $SlotMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class __$$_PivImportResultCopyWithImpl<$Res> + extends _$PivImportResultCopyWithImpl<$Res, _$_PivImportResult> + implements _$$_PivImportResultCopyWith<$Res> { + __$$_PivImportResultCopyWithImpl( + _$_PivImportResult _value, $Res Function(_$_PivImportResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? metadata = freezed, + Object? publicKey = freezed, + Object? certificate = freezed, + }) { + return _then(_$_PivImportResult( + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as SlotMetadata?, + publicKey: freezed == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String?, + certificate: freezed == certificate + ? _value.certificate + : certificate // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivImportResult implements _PivImportResult { + _$_PivImportResult( + {required this.metadata, + required this.publicKey, + required this.certificate}); + + factory _$_PivImportResult.fromJson(Map json) => + _$$_PivImportResultFromJson(json); + + @override + final SlotMetadata? metadata; + @override + final String? publicKey; + @override + final String? certificate; + + @override + String toString() { + return 'PivImportResult(metadata: $metadata, publicKey: $publicKey, certificate: $certificate)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivImportResult && + (identical(other.metadata, metadata) || + other.metadata == metadata) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.certificate, certificate) || + other.certificate == certificate)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, metadata, publicKey, certificate); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivImportResultCopyWith<_$_PivImportResult> get copyWith => + __$$_PivImportResultCopyWithImpl<_$_PivImportResult>(this, _$identity); + + @override + Map toJson() { + return _$$_PivImportResultToJson( + this, + ); + } +} + +abstract class _PivImportResult implements PivImportResult { + factory _PivImportResult( + {required final SlotMetadata? metadata, + required final String? publicKey, + required final String? certificate}) = _$_PivImportResult; + + factory _PivImportResult.fromJson(Map json) = + _$_PivImportResult.fromJson; + + @override + SlotMetadata? get metadata; + @override + String? get publicKey; + @override + String? get certificate; + @override + @JsonKey(ignore: true) + _$$_PivImportResultCopyWith<_$_PivImportResult> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/piv/models.g.dart b/lib/piv/models.g.dart new file mode 100644 index 00000000..109f280d --- /dev/null +++ b/lib/piv/models.g.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_PinMetadata _$$_PinMetadataFromJson(Map json) => + _$_PinMetadata( + json['default_value'] as bool, + json['total_attempts'] as int, + json['attempts_remaining'] as int, + ); + +Map _$$_PinMetadataToJson(_$_PinMetadata instance) => + { + 'default_value': instance.defaultValue, + 'total_attempts': instance.totalAttempts, + 'attempts_remaining': instance.attemptsRemaining, + }; + +_$_ManagementKeyMetadata _$$_ManagementKeyMetadataFromJson( + Map json) => + _$_ManagementKeyMetadata( + $enumDecode(_$ManagementKeyTypeEnumMap, json['key_type']), + json['default_value'] as bool, + $enumDecode(_$TouchPolicyEnumMap, json['touch_policy']), + ); + +Map _$$_ManagementKeyMetadataToJson( + _$_ManagementKeyMetadata instance) => + { + 'key_type': _$ManagementKeyTypeEnumMap[instance.keyType]!, + 'default_value': instance.defaultValue, + 'touch_policy': _$TouchPolicyEnumMap[instance.touchPolicy]!, + }; + +const _$ManagementKeyTypeEnumMap = { + ManagementKeyType.tdes: 3, + ManagementKeyType.aes128: 8, + ManagementKeyType.aes192: 10, + ManagementKeyType.aes256: 12, +}; + +const _$TouchPolicyEnumMap = { + TouchPolicy.dfault: 0, + TouchPolicy.never: 1, + TouchPolicy.always: 2, + TouchPolicy.cached: 3, +}; + +_$_SlotMetadata _$$_SlotMetadataFromJson(Map json) => + _$_SlotMetadata( + $enumDecode(_$KeyTypeEnumMap, json['key_type']), + $enumDecode(_$PinPolicyEnumMap, json['pin_policy']), + $enumDecode(_$TouchPolicyEnumMap, json['touch_policy']), + json['generated'] as bool, + json['public_key_encoded'] as String, + ); + +Map _$$_SlotMetadataToJson(_$_SlotMetadata instance) => + { + 'key_type': _$KeyTypeEnumMap[instance.keyType]!, + 'pin_policy': _$PinPolicyEnumMap[instance.pinPolicy]!, + 'touch_policy': _$TouchPolicyEnumMap[instance.touchPolicy]!, + 'generated': instance.generated, + 'public_key_encoded': instance.publicKeyEncoded, + }; + +const _$KeyTypeEnumMap = { + KeyType.rsa1024: 6, + KeyType.rsa2048: 7, + KeyType.eccp256: 17, + KeyType.eccp384: 20, +}; + +const _$PinPolicyEnumMap = { + PinPolicy.dfault: 0, + PinPolicy.never: 1, + PinPolicy.once: 2, + PinPolicy.always: 3, +}; + +_$_PivStateMetadata _$$_PivStateMetadataFromJson(Map json) => + _$_PivStateMetadata( + managementKeyMetadata: ManagementKeyMetadata.fromJson( + json['management_key_metadata'] as Map), + pinMetadata: + PinMetadata.fromJson(json['pin_metadata'] as Map), + pukMetadata: + PinMetadata.fromJson(json['puk_metadata'] as Map), + ); + +Map _$$_PivStateMetadataToJson(_$_PivStateMetadata instance) => + { + 'management_key_metadata': instance.managementKeyMetadata, + 'pin_metadata': instance.pinMetadata, + 'puk_metadata': instance.pukMetadata, + }; + +_$_PivState _$$_PivStateFromJson(Map json) => _$_PivState( + version: Version.fromJson(json['version'] as List), + authenticated: json['authenticated'] as bool, + derivedKey: json['derived_key'] as bool, + storedKey: json['stored_key'] as bool, + pinAttempts: json['pin_attempts'] as int, + chuid: json['chuid'] as String?, + ccc: json['ccc'] as String?, + metadata: json['metadata'] == null + ? null + : PivStateMetadata.fromJson(json['metadata'] as Map), + ); + +Map _$$_PivStateToJson(_$_PivState instance) => + { + 'version': instance.version, + 'authenticated': instance.authenticated, + 'derived_key': instance.derivedKey, + 'stored_key': instance.storedKey, + 'pin_attempts': instance.pinAttempts, + 'chuid': instance.chuid, + 'ccc': instance.ccc, + 'metadata': instance.metadata, + }; + +_$_CertInfo _$$_CertInfoFromJson(Map json) => _$_CertInfo( + subject: json['subject'] as String, + issuer: json['issuer'] as String, + serial: json['serial'] as String, + notValidBefore: json['not_valid_before'] as String, + notValidAfter: json['not_valid_after'] as String, + fingerprint: json['fingerprint'] as String, + ); + +Map _$$_CertInfoToJson(_$_CertInfo instance) => + { + 'subject': instance.subject, + 'issuer': instance.issuer, + 'serial': instance.serial, + 'not_valid_before': instance.notValidBefore, + 'not_valid_after': instance.notValidAfter, + 'fingerprint': instance.fingerprint, + }; + +_$_PivSlot _$$_PivSlotFromJson(Map json) => _$_PivSlot( + slot: SlotId.fromJson(json['slot'] as int), + hasKey: json['has_key'] as bool?, + certInfo: json['cert_info'] == null + ? null + : CertInfo.fromJson(json['cert_info'] as Map), + ); + +Map _$$_PivSlotToJson(_$_PivSlot instance) => + { + 'slot': _$SlotIdEnumMap[instance.slot]!, + 'has_key': instance.hasKey, + 'cert_info': instance.certInfo, + }; + +const _$SlotIdEnumMap = { + SlotId.authentication: 'authentication', + SlotId.signature: 'signature', + SlotId.keyManagement: 'keyManagement', + SlotId.cardAuth: 'cardAuth', +}; + +_$_ExamineResult _$$_ExamineResultFromJson(Map json) => + _$_ExamineResult( + password: json['password'] as bool, + privateKey: json['private_key'] as bool, + certificates: json['certificates'] as int, + $type: json['runtimeType'] as String?, + ); + +Map _$$_ExamineResultToJson(_$_ExamineResult instance) => + { + 'password': instance.password, + 'private_key': instance.privateKey, + 'certificates': instance.certificates, + 'runtimeType': instance.$type, + }; + +_$_InvalidPassword _$$_InvalidPasswordFromJson(Map json) => + _$_InvalidPassword( + $type: json['runtimeType'] as String?, + ); + +Map _$$_InvalidPasswordToJson(_$_InvalidPassword instance) => + { + 'runtimeType': instance.$type, + }; + +_$_PivGenerateResult _$$_PivGenerateResultFromJson(Map json) => + _$_PivGenerateResult( + generateType: $enumDecode(_$GenerateTypeEnumMap, json['generate_type']), + publicKey: json['public_key'] as String, + result: json['result'] as String, + ); + +Map _$$_PivGenerateResultToJson( + _$_PivGenerateResult instance) => + { + 'generate_type': _$GenerateTypeEnumMap[instance.generateType]!, + 'public_key': instance.publicKey, + 'result': instance.result, + }; + +const _$GenerateTypeEnumMap = { + GenerateType.certificate: 'certificate', + GenerateType.csr: 'csr', +}; + +_$_PivImportResult _$$_PivImportResultFromJson(Map json) => + _$_PivImportResult( + metadata: json['metadata'] == null + ? null + : SlotMetadata.fromJson(json['metadata'] as Map), + publicKey: json['public_key'] as String?, + certificate: json['certificate'] as String?, + ); + +Map _$$_PivImportResultToJson(_$_PivImportResult instance) => + { + 'metadata': instance.metadata, + 'public_key': instance.publicKey, + 'certificate': instance.certificate, + }; diff --git a/lib/piv/state.dart b/lib/piv/state.dart new file mode 100644 index 00000000..8ebb628c --- /dev/null +++ b/lib/piv/state.dart @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../app/models.dart'; +import '../core/state.dart'; +import 'models.dart'; + +final pivStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), +); + +abstract class PivStateNotifier extends ApplicationStateNotifier { + Future reset(); + + Future authenticate(String managementKey); + Future setManagementKey( + String managementKey, { + ManagementKeyType managementKeyType = defaultManagementKeyType, + bool storeKey = false, + }); + + Future verifyPin( + String pin); //TODO: Maybe return authenticated? + Future changePin(String pin, String newPin); + Future changePuk(String puk, String newPuk); + Future unblockPin(String puk, String newPin); +} + +final pivSlotsProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + () => throw UnimplementedError(), +); + +abstract class PivSlotsNotifier + extends AutoDisposeFamilyAsyncNotifier, DevicePath> { + Future examine(String data, {String? password}); + Future<(SlotMetadata?, String?)> read(SlotId slot); + Future generate( + SlotId slot, + KeyType keyType, { + required PivGenerateParameters parameters, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + String? pin, + }); + Future import( + SlotId slot, + String data, { + String? password, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + }); + Future delete(SlotId slot); +} diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart new file mode 100644 index 00000000..f6aa53c0 --- /dev/null +++ b/lib/piv/views/actions.dart @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/app/models.dart'; + +import '../../app/message.dart'; +import '../../app/shortcuts.dart'; +import '../../app/state.dart'; +import '../models.dart'; +import '../state.dart'; +import 'authentication_dialog.dart'; +import 'delete_certificate_dialog.dart'; +import 'generate_key_dialog.dart'; +import 'import_file_dialog.dart'; +import 'pin_dialog.dart'; + +class AuthenticateIntent extends Intent { + const AuthenticateIntent(); +} + +class VerifyPinIntent extends Intent { + const VerifyPinIntent(); +} + +class GenerateIntent extends Intent { + const GenerateIntent(); +} + +class ImportIntent extends Intent { + const ImportIntent(); +} + +class ExportIntent extends Intent { + const ExportIntent(); +} + +Future _authenticate( + WidgetRef ref, DevicePath devicePath, PivState pivState) async { + final withContext = ref.read(withContextProvider); + return await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => AuthenticationDialog( + devicePath, + pivState, + ), + ) ?? + false); +} + +Future _authIfNeeded( + WidgetRef ref, DevicePath devicePath, PivState pivState) async { + if (pivState.needsAuth) { + return await _authenticate(ref, devicePath, pivState); + } + return true; +} + +Widget registerPivActions( + DevicePath devicePath, + PivState pivState, + PivSlot pivSlot, { + required WidgetRef ref, + required Widget Function(BuildContext context) builder, + Map> actions = const {}, +}) => + Actions( + actions: { + AuthenticateIntent: CallbackAction( + onInvoke: (intent) => _authenticate(ref, devicePath, pivState), + ), + GenerateIntent: + CallbackAction(onInvoke: (intent) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + + final withContext = ref.read(withContextProvider); + + // TODO: Avoid asking for PIN if not needed? + final verified = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => PinDialog(devicePath))) ?? + false; + + if (!verified) { + return false; + } + + return await withContext((context) async { + final PivGenerateResult? result = await showBlurDialog( + context: context, + builder: (context) => GenerateKeyDialog( + devicePath, + pivState, + pivSlot, + ), + ); + + switch (result?.generateType) { + case GenerateType.csr: + final filePath = await FilePicker.platform.saveFile( + dialogTitle: 'Save CSR to file', + allowedExtensions: ['csr'], + type: FileType.custom, + lockParentWindow: true, + ); + if (filePath != null) { + final file = File(filePath); + await file.writeAsString(result!.result, flush: true); + } + break; + default: + break; + } + + return result != null; + }); + }), + ImportIntent: CallbackAction(onInvoke: (intent) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + + final picked = await FilePicker.platform.pickFiles( + allowedExtensions: ['pem', 'der', 'pfx', 'p12', 'key', 'crt'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + dialogTitle: 'Select file to import'); + if (picked == null || picked.files.isEmpty) { + return false; + } + + final withContext = ref.read(withContextProvider); + return await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => ImportFileDialog( + devicePath, + pivState, + pivSlot, + File(picked.paths.first!), + ), + ) ?? + false); + }), + ExportIntent: CallbackAction(onInvoke: (intent) async { + final (_, cert) = await ref + .read(pivSlotsProvider(devicePath).notifier) + .read(pivSlot.slot); + + if (cert == null) { + return false; + } + + final filePath = await FilePicker.platform.saveFile( + dialogTitle: 'Export certificate to file', + allowedExtensions: ['pem'], + type: FileType.custom, + lockParentWindow: true, + ); + if (filePath == null) { + return false; + } + + final file = File(filePath); + await file.writeAsString(cert, flush: true); + + await ref.read(withContextProvider)((context) async { + showMessage(context, 'Certificate exported'); + }); + return true; + }), + DeleteIntent: CallbackAction(onInvoke: (_) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + final withContext = ref.read(withContextProvider); + final bool? deleted = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteCertificateDialog( + devicePath, + pivSlot, + ), + ) ?? + false); + + // Needs to move to slot dialog(?) or react to state change + // Pop the slot dialog if deleted + if (deleted == true) { + await withContext((context) async { + Navigator.of(context).pop(); + }); + } + return deleted; + }), //TODO + ...actions, + }, + child: Builder(builder: builder), + ); diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart new file mode 100644 index 00000000..aabd5f36 --- /dev/null +++ b/lib/piv/views/authentication_dialog.dart @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class AuthenticationDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + const AuthenticationDialog(this.devicePath, this.pivState, {super.key}); + + @override + ConsumerState createState() => + _AuthenticationDialogState(); +} + +class _AuthenticationDialogState extends ConsumerState { + String _managementKey = ''; + bool _keyIsWrong = false; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text("Unlock management functions"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .authenticate(_managementKey); + if (status) { + navigator.pop(true); + } else { + setState(() { + _keyIsWrong = true; + }); + } + } on CancellationException catch (_) { + navigator.pop(false); + } catch (_) { + // TODO: More error cases + setState(() { + _keyIsWrong = true; + }); + } + }, + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Management key", + prefixIcon: const Icon(Icons.key_outlined), + errorText: _keyIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _keyIsWrong = false; + _managementKey = value; + }); + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/delete_certificate_dialog.dart b/lib/piv/views/delete_certificate_dialog.dart new file mode 100644 index 00000000..57f908b1 --- /dev/null +++ b/lib/piv/views/delete_certificate_dialog.dart @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class DeleteCertificateDialog extends ConsumerWidget { + final DevicePath devicePath; + final PivSlot pivSlot; + const DeleteCertificateDialog(this.devicePath, this.pivSlot, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_delete_account), + actions: [ + TextButton( + key: keys.deleteButton, + onPressed: () async { + try { + await ref + .read(pivSlotsProvider(devicePath).notifier) + .delete(pivSlot.slot); + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_account_deleted); + }, + ); + } on CancellationException catch (_) { + // ignored + } + }, + child: Text(l10n.s_delete), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_warning_delete_account), + Text( + l10n.p_warning_disable_credential, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text(// TODO + 'Delete certificate in ${pivSlot.slot.getDisplayName(l10n)} (Slot ${pivSlot.slot.id.toRadixString(16).padLeft(2, '0')})?'), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart new file mode 100644 index 00000000..ca83dbc8 --- /dev/null +++ b/lib/piv/views/generate_key_dialog.dart @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../core/models.dart'; +import '../../widgets/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class GenerateKeyDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + final PivSlot pivSlot; + const GenerateKeyDialog(this.devicePath, this.pivState, this.pivSlot, + {super.key}); + + @override + ConsumerState createState() => + _GenerateKeyDialogState(); +} + +class _GenerateKeyDialogState extends ConsumerState { + String _subject = ''; + GenerateType _generateType = defaultGenerateType; + KeyType _keyType = defaultKeyType; + late DateTime _validFrom; + late DateTime _validTo; + late DateTime _validToDefault; + late DateTime _validToMax; + + @override + void initState() { + super.initState(); + + final now = DateTime.now(); + _validFrom = DateTime.utc(now.year, now.month, now.day); + _validToDefault = DateTime.utc(now.year + 1, now.month, now.day); + _validTo = _validToDefault; + _validToMax = DateTime.utc(now.year + 10, now.month, now.day); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final navigator = Navigator.of(context); + return ResponsiveDialog( + title: Text("Generate key"), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: () async { + final result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .generate( + widget.pivSlot.slot, + _keyType, + parameters: switch (_generateType) { + GenerateType.certificate => + PivGenerateParameters.certificate( + subject: _subject, + validFrom: _validFrom, + validTo: _validTo), + GenerateType.csr => + PivGenerateParameters.csr(subject: _subject), + }, + ); + + navigator.pop(result); + }, + child: Text(l10n.s_save), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + key: keys.subjectField, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Subject", + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _subject = value; + }); + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + items: GenerateType.values, + value: _generateType, + selected: _generateType != defaultGenerateType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _generateType = value; + }); + }, + ), + ChoiceFilterChip( + items: KeyType.values, + value: _keyType, + selected: _keyType != defaultKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _keyType = value; + }); + }, + ), + if (_generateType == GenerateType.certificate) + FilterChip( + label: Text(dateFormatter.format(_validTo)), + onSelected: (value) async { + final selected = await showDatePicker( + context: context, + initialDate: _validTo, + firstDate: _validFrom, + lastDate: _validToMax, + ); + if (selected != null) { + setState(() { + _validTo = selected; + }); + } + }, + ), + ]), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart new file mode 100644 index 00000000..d85bb991 --- /dev/null +++ b/lib/piv/views/import_file_dialog.dart @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class ImportFileDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + final PivSlot pivSlot; + final File file; + const ImportFileDialog( + this.devicePath, this.pivState, this.pivSlot, this.file, + {super.key}); + + @override + ConsumerState createState() => + _ImportFileDialogState(); +} + +class _ImportFileDialogState extends ConsumerState { + late String _data; + PivExamineResult? _state; + String _password = ''; + bool _passwordIsWrong = false; + + @override + void initState() { + super.initState(); + _init(); + } + + void _init() async { + final bytes = await widget.file.readAsBytes(); + _data = bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); + _examine(); + } + + void _examine() async { + setState(() { + _state = null; + }); + final result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .examine(_data, password: _password.isNotEmpty ? _password : null); + setState(() { + _state = result; + _passwordIsWrong = result.maybeWhen( + invalidPassword: () => _password.isNotEmpty, + orElse: () => true, + ); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final state = _state; + if (state == null) { + return ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: null, + child: Text(l10n.s_unlock), + ), + ], + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0), + child: Center( + child: CircularProgressIndicator(), + )), + ); + } + + return state.when( + invalidPassword: () => ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () => _examine(), + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Password", + prefixIcon: const Icon(Icons.password_outlined), + errorText: _passwordIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _passwordIsWrong = false; + _password = value; + }); + }, + onSubmitted: (_) => _examine(), + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ), + result: (_, privateKey, certificates) => ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () async { + final navigator = Navigator.of(context); + try { + await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .import(widget.pivSlot.slot, _data, + password: _password.isNotEmpty ? _password : null); + navigator.pop(true); + } catch (_) { + // TODO: More error cases + setState(() { + _passwordIsWrong = true; + }); + } + }, + child: Text(l10n.s_save), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Import the following into slot ${widget.pivSlot.slot.getDisplayName(l10n)}?"), + if (privateKey) Text("- Private key"), + if (certificates > 0) Text("- Certificate"), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ), + ); + } +} diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart new file mode 100644 index 00000000..68b833a8 --- /dev/null +++ b/lib/piv/views/key_actions.dart @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import 'manage_key_dialog.dart'; +import 'manage_pin_puk_dialog.dart'; +import 'reset_dialog.dart'; + +Widget pivBuildActions(BuildContext context, DevicePath devicePath, + PivState pivState, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + + final usingDefaultMgmtKey = + pivState.metadata?.managementKeyMetadata.defaultValue == true; + + final pinBlocked = pivState.pinAttempts == 0; + + return FsDialog( + child: Column( + children: [ + ListTitle(l10n.s_manage, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.managePinAction, + title: Text(l10n.s_pin), + subtitle: Text(pinBlocked + ? 'Blocked, use PUK to reset' + : '${pivState.pinAttempts} attempts remaining'), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.pin_outlined), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: pinBlocked ? ManageTarget.unblock : ManageTarget.pin, + ), + ); + }), + ListTile( + key: keys.managePukAction, + title: Text('PUK'), // TODO + subtitle: Text( + '${pivState.metadata?.pukMetadata.attemptsRemaining ?? '?'} attempts remaining'), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.pin_outlined), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => + ManagePinPukDialog(devicePath, target: ManageTarget.puk), + ); + }), + ListTile( + key: keys.manageManagementKeyAction, + title: Text('Management Key'), // TODO + subtitle: Text(usingDefaultMgmtKey + ? 'Warning: Default key used' + : (pivState.protectedKey + ? 'PIN can be used instead' + : 'Change your management key')), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.key_outlined), + ), + trailing: + usingDefaultMgmtKey ? const Icon(Icons.warning_amber) : null, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManageKeyDialog(devicePath, pivState), + ); + }), + ListTile( + key: keys.resetAction, + title: Text('Reset PIV'), //TODO + subtitle: Text(l10n.l_factory_reset_this_app), + leading: CircleAvatar( + foregroundColor: theme.onError, + backgroundColor: theme.error, + child: const Icon(Icons.delete_outline), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }), + ListTitle(l10n.s_setup, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.setupMacOsAction, + title: Text('Setup for macOS'), //TODO + subtitle: Text('Create certificates for macOS login'), //TODO + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.laptop), + ), + onTap: () async { + Navigator.of(context).pop(); + }), + ], + ), + ); +} diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart new file mode 100644 index 00000000..faf12dac --- /dev/null +++ b/lib/piv/views/manage_key_dialog.dart @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2022 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'pin_dialog.dart'; + +class ManageKeyDialog extends ConsumerStatefulWidget { + final DevicePath path; + final PivState pivState; + const ManageKeyDialog(this.path, this.pivState, {super.key}); + + @override + ConsumerState createState() => + _ManageKeyDialogState(); +} + +class _ManageKeyDialogState extends ConsumerState { + late bool _defaultKeyUsed; + late bool _usesStoredKey; + late bool _storeKey; + String _currentKeyOrPin = ''; + bool _currentIsWrong = false; + int _attemptsRemaining = -1; + String _newKey = ''; + ManagementKeyType _keyType = ManagementKeyType.tdes; + + @override + void initState() { + super.initState(); + + _defaultKeyUsed = + widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false; + _usesStoredKey = widget.pivState.protectedKey; + if (!_usesStoredKey && _defaultKeyUsed) { + _currentKeyOrPin = defaultManagementKey; + } + _storeKey = _usesStoredKey; + } + + _submit() async { + final notifier = ref.read(pivStateProvider(widget.path).notifier); + if (_usesStoredKey) { + final status = (await notifier.verifyPin(_currentKeyOrPin)).when( + success: () => true, + failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + }); + return false; + }, + ); + if (!status) { + return; + } + } else { + if (!await notifier.authenticate(_currentKeyOrPin)) { + setState(() { + _currentIsWrong = true; + }); + return; + } + } + + if (_storeKey && !_usesStoredKey) { + final withContext = ref.read(withContextProvider); + final verified = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => PinDialog(widget.path))) ?? + false; + + if (!verified) { + return; + } + } + + print("Set new key: $_newKey"); + await notifier.setManagementKey(_newKey, + managementKeyType: _keyType, storeKey: _storeKey); + if (!mounted) return; + showMessage(context, "Management key changed"); + + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final currentType = widget.pivState.metadata?.managementKeyMetadata.keyType; + final hexLength = _keyType.keyLength * 2; + + return ResponsiveDialog( + title: Text('Change Management Key'), + actions: [ + TextButton( + onPressed: _submit, + key: keys.saveButton, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_enter_current_password_or_reset), + if (widget.pivState.protectedKey) + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "PIN", + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _currentIsWrong + ? "Wrong PIN ($_attemptsRemaining attempts left)" + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentKeyOrPin = value; + }); + }, + ), + if (!widget.pivState.protectedKey) + TextFormField( + key: keys.pinPukField, + autofocus: !_defaultKeyUsed, + autofillHints: const [AutofillHints.password], + initialValue: _defaultKeyUsed ? defaultManagementKey : null, + readOnly: _defaultKeyUsed, + maxLength: !_defaultKeyUsed && currentType != null + ? currentType.keyLength * 2 + : null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Current management key', + prefixIcon: const Icon(Icons.password_outlined), + errorText: _currentIsWrong ? 'Wrong key' : null, + errorMaxLines: 3, + helperText: + _defaultKeyUsed ? "Default management key used" : null, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentKeyOrPin = value; + }); + }, + ), + Text("Enter your new management key."), + TextField( + key: keys.newPinPukField, + autofocus: _defaultKeyUsed, + autofillHints: const [AutofillHints.newPassword], + maxLength: hexLength, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "New management key", + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentKeyOrPin.isNotEmpty, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _newKey = value; + }); + }, + onSubmitted: (_) { + if (_newKey.length == hexLength) { + _submit(); + } + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + if (currentType != null) + ChoiceFilterChip( + items: ManagementKeyType.values, + value: _keyType, + selected: _keyType != defaultManagementKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _keyType = value; + }); + }, + ), + FilterChip( + label: Text("Protect with PIN"), + selected: _storeKey, + onSelected: (value) { + setState(() { + _storeKey = value; + }); + }, + ), + ]), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart new file mode 100644 index 00000000..eb7f455c --- /dev/null +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +enum ManageTarget { pin, puk, unblock } + +class ManagePinPukDialog extends ConsumerStatefulWidget { + final DevicePath path; + final ManageTarget target; + const ManagePinPukDialog(this.path, + {super.key, this.target = ManageTarget.pin}); + + @override + ConsumerState createState() => + _ManagePinPukDialogState(); +} + +//TODO: Use switch expressions in Dart 3 +class _ManagePinPukDialogState extends ConsumerState { + String _currentPin = ''; + String _newPin = ''; + String _confirmPin = ''; + bool _currentIsWrong = false; + int _attemptsRemaining = -1; + + _submit() async { + final notifier = ref.read(pivStateProvider(widget.path).notifier); + final PinVerificationStatus result; + switch (widget.target) { + case ManageTarget.pin: + result = await notifier.changePin(_currentPin, _newPin); + break; + case ManageTarget.puk: + result = await notifier.changePuk(_currentPin, _newPin); + break; + case ManageTarget.unblock: + result = await notifier.unblockPin(_currentPin, _newPin); + break; + } + + result.when(success: () { + if (!mounted) return; + Navigator.of(context).pop(); + showMessage(context, AppLocalizations.of(context)!.s_password_set); + }, failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + _currentPin = ''; + }); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final isValid = + _newPin.isNotEmpty && _newPin == _confirmPin && _currentPin.isNotEmpty; + + final String titleText; + switch (widget.target) { + case ManageTarget.pin: + titleText = "Change PIN"; + break; + case ManageTarget.puk: + titleText = l10n.s_manage_password; + break; + case ManageTarget.unblock: + titleText = "Unblock PIN"; + break; + } + + return ResponsiveDialog( + title: Text(titleText), + actions: [ + TextButton( + onPressed: isValid ? _submit : null, + key: keys.saveButton, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_enter_current_password_or_reset), + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.pinPukField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? 'Current PIN' + : 'Current PUK', + prefixIcon: const Icon(Icons.password_outlined), + errorText: _currentIsWrong + ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentPin = value; + }); + }, + ), + Text( + "Enter your new ${widget.target == ManageTarget.puk ? 'PUK' : 'PIN'}. Must be 6-8 characters."), + TextField( + key: keys.newPinPukField, + obscureText: true, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.puk + ? "New PUK" + : l10n.s_new_pin, + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentPin.isNotEmpty, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _newPin = value; + }); + }, + onSubmitted: (_) { + if (isValid) { + _submit(); + } + }, + ), + TextField( + key: keys.confirmPinPukField, + obscureText: true, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_confirm_pin, + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentPin.isNotEmpty && _newPin.isNotEmpty, + ), + textInputAction: TextInputAction.done, + onChanged: (value) { + setState(() { + _confirmPin = value; + }); + }, + onSubmitted: (_) { + if (isValid) { + _submit(); + } + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart new file mode 100644 index 00000000..84485f8f --- /dev/null +++ b/lib/piv/views/pin_dialog.dart @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class PinDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + const PinDialog(this.devicePath, {super.key}); + + @override + ConsumerState createState() => _PinDialogState(); +} + +class _PinDialogState extends ConsumerState { + String _pin = ''; + bool _pinIsWrong = false; + int _attemptsRemaining = -1; + + Future _submit() async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .verifyPin(_pin); + status.when( + success: () { + navigator.pop(true); + }, + failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _pinIsWrong = true; + }); + }, + ); + } on CancellationException catch (_) { + navigator.pop(false); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text("PIN required"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: _submit, + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "PIN", + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _pinIsWrong + ? "Wrong PIN ($_attemptsRemaining attempts left)" + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _pinIsWrong = false; + _pin = value; + }); + }, + onSubmitted: (_) => _submit(), + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart new file mode 100644 index 00000000..39550804 --- /dev/null +++ b/lib/piv/views/piv_screen.dart @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/shortcuts.dart'; +import '../../app/views/app_failure_page.dart'; +import '../../app/views/app_page.dart'; +import '../../app/views/message_page.dart'; +import '../models.dart'; +import '../state.dart'; +import 'key_actions.dart'; +import 'slot_dialog.dart'; + +class PivScreen extends ConsumerWidget { + final DevicePath devicePath; + + const PivScreen(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ref.watch(pivStateProvider(devicePath)).when( + loading: () => MessagePage( + title: Text(l10n.s_authenticator), + graphic: const CircularProgressIndicator(), + delayedContent: true, + ), + error: (error, _) => AppFailurePage( + title: Text(l10n.s_authenticator), + cause: error, + ), + data: (pivState) { + final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; + return AppPage( + title: const Text('PIV'), + keyActionsBuilder: (context) => + pivBuildActions(context, devicePath, pivState, ref), + child: Column( + children: [ + if (pivSlots?.hasValue == true) + ...pivSlots!.value.map((e) => Actions( + actions: { + OpenIntent: + CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + builder: (context) => + SlotDialog(pivState, e.slot), + ); + return null; + }), + }, + child: _CertificateListItem(e), + )) + ], + ), + ); + }, + ); + } +} + +class _CertificateListItem extends StatelessWidget { + final PivSlot pivSlot; + const _CertificateListItem(this.pivSlot); + + @override + Widget build(BuildContext context) { + final slot = pivSlot.slot; + final certInfo = pivSlot.certInfo; + final l10n = AppLocalizations.of(context)!; + final colorScheme = Theme.of(context).colorScheme; + return ListTile( + leading: CircleAvatar( + foregroundColor: colorScheme.onSecondary, + backgroundColor: colorScheme.secondary, + child: const Icon(Icons.approval), + ), + title: Text( + '${slot.getDisplayName(l10n)} (Slot ${slot.id.toRadixString(16).padLeft(2, '0')})', + softWrap: false, + overflow: TextOverflow.fade, + ), + subtitle: certInfo != null + ? Text( + 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + softWrap: false, + overflow: TextOverflow.fade, + ) + : Text(pivSlot.hasKey == true + ? 'Key without certificate loaded' + : 'No certificate loaded'), + trailing: OutlinedButton( + onPressed: () { + Actions.maybeInvoke(context, const OpenIntent()); + }, + child: const Icon(Icons.more_horiz), + ), + ); + } +} diff --git a/lib/piv/views/reset_dialog.dart b/lib/piv/views/reset_dialog.dart new file mode 100644 index 00000000..8bac4186 --- /dev/null +++ b/lib/piv/views/reset_dialog.dart @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../state.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; + +class ResetDialog extends ConsumerWidget { + final DevicePath devicePath; + const ResetDialog(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_factory_reset), + actions: [ + TextButton( + onPressed: () async { + await ref.read(pivStateProvider(devicePath).notifier).reset(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.l_oath_application_reset); //TODO + }); + }, + child: Text(l10n.s_reset), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + children: [ + Text( + l10n.p_warning_factory_reset, // TODO + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(l10n.p_warning_disable_credentials), //TODO + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart new file mode 100644 index 00000000..9e04683d --- /dev/null +++ b/lib/piv/views/slot_dialog.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import '../state.dart'; +import 'actions.dart'; + +class SlotDialog extends ConsumerWidget { + final PivState pivState; + final SlotId pivSlot; + const SlotDialog(this.pivState, this.pivSlot, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: Solve this in a cleaner way + final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node; + if (node == null) { + // The rest of this method assumes there is a device, and will throw an exception if not. + // This will never be shown, as the dialog will be immediately closed + return const SizedBox(); + } + + final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + + final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => + value.whenOrNull( + data: (data) => + data.firstWhere((element) => element.slot == pivSlot)))); + + if (slotData == null) { + return const FsDialog(child: CircularProgressIndicator()); + } + + final certInfo = slotData.certInfo; + return registerPivActions( + node.path, + pivState, + slotData, + ref: ref, + builder: (context) => FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 32), + child: Column( + children: [ + Text( + '${pivSlot.getDisplayName(l10n)} (Slot ${pivSlot.id.toRadixString(16).padLeft(2, '0')})', + style: textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + if (certInfo != null) ...[ + Text( + 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Serial: ${certInfo.serial}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Fingerprint: ${certInfo.fingerprint}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Not before: ${certInfo.notValidBefore}, Not after: ${certInfo.notValidAfter}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'No certificate loaded', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + ), + ], + const SizedBox(height: 16), + ], + ), + ), + ListTitle(AppLocalizations.of(context)!.s_actions, + textStyle: textTheme.bodyLarge), + _SlotDialogActions(certInfo), + ], + ), + ), + ), + ); + } +} + +class _SlotDialogActions extends StatelessWidget { + final CertInfo? certInfo; + const _SlotDialogActions(this.certInfo); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + child: const Icon(Icons.add_outlined), + ), + title: Text('Generate key'), + subtitle: Text('Generate a new certificate or CSR'), + onTap: () { + Actions.invoke(context, const GenerateIntent()); + }, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.file_download_outlined), + ), + title: Text('Import file'), + subtitle: Text('Import a key and/or certificate from file'), + onTap: () { + Actions.invoke(context, const ImportIntent()); + }, + ), + if (certInfo != null) ...[ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.file_upload_outlined), + ), + title: Text('Export certificate'), + subtitle: Text('Export the certificate to file'), + onTap: () { + Actions.invoke(context, const ExportIntent()); + }, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.error, + foregroundColor: theme.onError, + child: const Icon(Icons.delete_outline), + ), + title: Text('Delete certificate'), + subtitle: Text('Remove the certificate from the YubiKey'), + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e62a2742..0291513d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -384,10 +384,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.7.1" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 17ae1a01..f1801cea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,7 +80,7 @@ dev_dependencies: build_runner: ^2.4.5 freezed: ^2.3.5 - json_serializable: ^6.5.4 + json_serializable: ^6.7.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec