From ba3e133a46873d760022f7694d67a1a7ce13416a Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Tue, 15 Sep 2026 17:18:47 +0800 Subject: [PATCH 01/19] feat: begin migrating PIV operations to libcanokey --- .github/workflows/usbip.yml | 8 +- README.md | 5 + docs/libcanokey-migration.md | 71 +++ .../applets/piv/piv_controller.dart | 21 +- lib/helper/utils/piv_card.dart | 35 +- lib/helper/utils/protocol_operation.dart | 52 ++ lib/src/rust/api/protocol.dart | 90 +++ lib/src/rust/frb_generated.dart | 603 +++++++++++++++++- lib/src/rust/frb_generated.io.dart | 201 +++++- lib/src/rust/frb_generated.web.dart | 195 +++++- rust/Cargo.lock | 171 ++++- rust/Cargo.toml | 1 + rust/THIRD_PARTY_LICENSES.json | 84 +++ rust/src/api/mod.rs | 1 + rust/src/api/protocol.rs | 224 +++++++ rust/src/frb_generated.rs | 564 ++++++++++++++-- .../piv/piv_certificate_loading_test.dart | 41 +- test/helper/utils/piv_card_test.dart | 36 +- .../helper/utils/protocol_operation_test.dart | 135 ++++ test/usbip/console_smoke.dart | 3 + 20 files changed, 2427 insertions(+), 114 deletions(-) create mode 100644 docs/libcanokey-migration.md create mode 100644 lib/helper/utils/protocol_operation.dart create mode 100644 lib/src/rust/api/protocol.dart create mode 100644 rust/src/api/protocol.rs create mode 100644 test/helper/utils/protocol_operation_test.dart diff --git a/.github/workflows/usbip.yml b/.github/workflows/usbip.yml index 032b520..e005ffa 100644 --- a/.github/workflows/usbip.yml +++ b/.github/workflows/usbip.yml @@ -78,10 +78,14 @@ jobs: - run: flutter pub get - - name: Test FIDO2 backend and client integration + - name: Test native backends and client integration env: FIDO2_CRYPTO_LIBRARY: ${{ github.workspace }}/build/usbip-backend/librust_lib_canokey_console.so - run: flutter test --no-pub test/helper/utils/fido2_backend_test.dart + FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR: ${{ github.workspace }}/build/usbip-backend + run: >- + flutter test --no-pub + test/helper/utils/fido2_backend_test.dart + test/helper/utils/piv_card_test.dart - name: Share native backend with firmware jobs uses: actions/upload-artifact@v7 diff --git a/README.md b/README.md index aadc7b7..c52ca02 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,11 @@ Its native C ABI is linked into the existing console Rust library. The wasm-bindgen dependency family is pinned to versions compatible with fido2 2.0.0. Web uses the separate JS/WASM loader distributed with the Dart package. +PIV selection, version and algorithm-configuration reads now use a pinned +libcanokey Rust dependency through Flutter Rust Bridge. See the +[migration boundary and next steps](docs/libcanokey-migration.md) for the +implemented scope, session requirements and validation commands. + Before running backend or USB/IP tests locally, build the native backend: ```bash diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md new file mode 100644 index 0000000..911bf90 --- /dev/null +++ b/docs/libcanokey-migration.md @@ -0,0 +1,71 @@ +# libcanokey migration + +The first increment uses the Rust facade through Flutter Rust Bridge, following +[canokey-pkcs11#5](https://github.com/canokeys/canokey-pkcs11/pull/5). Cargo pins +libcanokey to the same `10808e1760bf5efa86b5ff2e0b4acbeaba441edc` revision. +The C ABI is unnecessary in Console's existing Rust library. + +## Implemented + +- PIV selection uses `select_application`, including an explicit Le for legacy + readers. This is still an explicit caller action and may clear authentication. +- PIV version and algorithm configuration reads use the upstream selected-app + operations. Neither inserts SELECT, probes the device, or authenticates. +- An opaque FRB operation owns its protocol state across Dart await points. + The Dart executor exchanges complete APDUs, including response status words. + libcanokey owns continuation, safe Le correction, budgets and validation. +- Completion, protocol failures, malformed transport hex and transport exceptions + all close/dispose the operation. No mutation is retried or rolled back. + Byte buffers copied to Dart are wiped after each exchange; immutable transport + strings and existing diagnostic logs are not erasable by this executor. +- Errors retain category, phase, status word, credential reference and retry count + as separate fields. Transport errors are propagated without conversion. +- Only an unsupported algorithm-configuration instruction permits the existing + firmware defaults. Security failures, malformed responses and communication + errors no longer silently choose default algorithms. + +Existing UI models still map validated configuration bytes to display/algorithm +fields. Other Admin/PIV/OATH/OpenPGP/NDEF/Pass operations remain in Dart. + +## Session boundary and next increments + +The caller must hold its selected card session for the entire executor future. +The executor does not acquire a connection or establish a transaction. Current +callers continue to use Console's existing SmartCard workflows; this increment +does not add a global connection lock or profile cache. + +Before migrating authenticated operations, establish a connection-bound exclusive +lease with generation checks, and derive a profile from observations of that same +device. Keep the lease across SELECT, authentication, dependent operations and +publication of results. Never SELECT/probe between authentication and its target. +Use upstream `*_in_context` factories for the already selected session; do not +assert authentication from cached UI state. Drain failed/timed-out I/O before +reusing the connection. Operation close is local cleanup, not card rollback. + +Suggested next steps are profile/session ownership, certificate and metadata +reads, PIN/PUK and management authentication, PIV writes/private operations, then +Admin/OATH/OpenPGP. Host CSR construction and UI policy remain in Console. + +## Verification + +```sh +flutter_rust_bridge_codegen generate +cargo test --manifest-path rust/Cargo.toml api::protocol --locked +cargo build --manifest-path rust/Cargo.toml --release --locked +flutter test --no-pub test/helper/utils/piv_card_test.dart +flutter test --no-pub --exclude-tags native +``` + +The native PIV transcript tests run alongside FIDO2 tests in the USB/IP workflow. +The firmware smoke entry point initializes FRB before the migrated PIV operations. +Native transcripts do not establish physical-device or legacy-firmware coverage. + +Local validation on 2026-09-15 (Rust 1.98.1 / Flutter 3.47.1): all 35 Rust tests, +292 non-native Dart/UI tests and 19 native PIV/FIDO2 tests passed. The generated +bridge built natively, `cargo check --target wasm32-unknown-unknown` and +`flutter build web --no-pub` passed. Changed Dart files passed analysis and the +workflow passed actionlint. Strict Clippy encountered the existing +`chunks_exact_to_as_chunks` lint in `rust/src/api/decode.rs:10`; all-target Clippy +passed with only that lint allowed. Repository-wide rustfmt also reports existing +formatting in `piv_crypto.rs`; the new protocol module passes rustfmt. No physical +card, browser transport session or USB/IP firmware matrix was run locally. diff --git a/lib/controller/applets/piv/piv_controller.dart b/lib/controller/applets/piv/piv_controller.dart index f0bd6df..037e717 100644 --- a/lib/controller/applets/piv/piv_controller.dart +++ b/lib/controller/applets/piv/piv_controller.dart @@ -150,20 +150,17 @@ class PivController extends PollingController { } Future _refreshCapabilities() async { - try { - final config = await _client.readAlgorithmExtensions(); - if (config == null) { - throw StateError('PIV algorithm extensions are unavailable'); - } + final config = await _client.readAlgorithmExtensions(); + if (config != null) { algorithmExtensionConfig = config; - } catch (_) { - final isLegacyV2 = - firmwareVersion.compareTo(const FirmwareVersion(2, 0, 0)) >= 0 && - firmwareVersion.compareTo(const FirmwareVersion(3, 0, 0)) < 0; - algorithmExtensionConfig = isLegacyV2 - ? PivAlgorithmExtensionConfig.legacyV2 - : PivAlgorithmExtensionConfig.defaults; + return; } + final isLegacyV2 = + firmwareVersion.compareTo(const FirmwareVersion(2, 0, 0)) >= 0 && + firmwareVersion.compareTo(const FirmwareVersion(3, 0, 0)) < 0; + algorithmExtensionConfig = isLegacyV2 + ? PivAlgorithmExtensionConfig.legacyV2 + : PivAlgorithmExtensionConfig.defaults; } Future _readMetadataDirectory() async { diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index 1d4dbba..0a4e3b6 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -4,24 +4,33 @@ import 'dart:typed_data'; import 'package:canokey_console/helper/tlv.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:canokey_console/models/piv.dart'; import 'package:convert/convert.dart'; class PivCardClient { - PivCardClient({ApduTransport transport = const SmartCardApduTransport()}) - : _transport = transport; + PivCardClient({ + ApduTransport transport = const SmartCardApduTransport(), + PivReadExecutor? readExecutor, + }) : _transport = transport, + _readExecutor = + readExecutor ?? + ((kind) => executeProtocolOperation( + ProtocolOperation.pivRead(kind: kind), + transport, + )); final ApduTransport _transport; + final PivReadExecutor _readExecutor; String? lastStatusWord; Future select() async { - SmartCard.assertOK(await _transport.transceive('00A4040005A000000308')); + await _readExecutor(PivReadOperation.select); } Future readVersion() async { - final response = await _transport.transceive('00FD000000'); - SmartCard.assertOK(response); - return Uint8List.fromList(hex.decode(SmartCard.dropSW(response))); + return _readExecutor(PivReadOperation.version); } Future readSerial() async { @@ -98,11 +107,15 @@ class PivCardClient { Future readAlgorithmExtensions() async { await select(); - final response = await _transport.transceive('00EE010000'); - if (!SmartCard.isOK(response)) return null; - return PivAlgorithmExtensionConfig.decode( - hex.decode(SmartCard.dropSW(response)), - ); + try { + final data = await _readExecutor(PivReadOperation.algorithmConfiguration); + return PivAlgorithmExtensionConfig.decode(data); + } on ProtocolException catch (error) { + // Only an unavailable instruction permits the existing firmware fallback. + // Security, malformed-data and unexpected card failures remain errors. + if (error.details.kind == 'UnsupportedFeature') return null; + rethrow; + } } Future readMetadata( diff --git a/lib/helper/utils/protocol_operation.dart b/lib/helper/utils/protocol_operation.dart new file mode 100644 index 0000000..cc63059 --- /dev/null +++ b/lib/helper/utils/protocol_operation.dart @@ -0,0 +1,52 @@ +import 'dart:typed_data'; + +import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; +import 'package:convert/convert.dart'; + +/// Protocol failures retain card status/context; transport exceptions pass through. +class ProtocolException implements Exception { + const ProtocolException(this.details); + + final ProtocolError details; + + @override + String toString() => + 'libcanokey: ${details.kind} during ${details.phase}' + '${details.statusWord == null ? '' : ' (SW=${details.statusWord!.toRadixString(16).padLeft(4, '0').toUpperCase()})'}'; +} + +/// The caller retains its selected card session for this entire future. +/// This executor only performs raw exchanges; libcanokey owns continuation, +/// bounded response parsing, and safe Le correction. No implicit replay occurs. +Future executeProtocolOperation( + ProtocolOperation operation, + ApduTransport transport, +) async { + try { + var step = operation.start(); + while (true) { + if (step.error case final error?) throw ProtocolException(error); + if (step.data case final data?) return data; + final command = step.command; + if (command == null) throw StateError('Missing libcanokey command'); + Uint8List? response; + try { + response = Uint8List.fromList( + hex.decode( + await transport.transceive(hex.encode(command).toUpperCase()), + ), + ); + step = operation.advance(response: response); + } finally { + command.fillRange(0, command.length, 0); + response?.fillRange(0, response.length, 0); + } + } + } finally { + operation.close(); + operation.dispose(); + } +} + +typedef PivReadExecutor = Future Function(PivReadOperation kind); diff --git a/lib/src/rust/api/protocol.dart b/lib/src/rust/api/protocol.dart new file mode 100644 index 0000000..2c125cc --- /dev/null +++ b/lib/src/rust/api/protocol.dart @@ -0,0 +1,90 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.13.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `drive`, `drive`, `failure` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Inner` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `from` + +// Rust type: RustOpaqueMoi> +abstract class ProtocolOperation implements RustOpaqueInterface { + /// Consume one complete, unprocessed response including SW1/SW2. + /// The input is copied into zeroizing storage and released before returning. + ProtocolStep advance({required List response}); + + /// Release locally, including on a transport exception. Never retry/rollback. + void close(); + + /// Construct without I/O. Only Select changes applet selection; reads never + /// probe, SELECT, authenticate, or retain a device profile implicitly. + static ProtocolOperation pivRead({required PivReadOperation kind}) => + RustLib.instance.api.crateApiProtocolProtocolOperationPivRead(kind: kind); + + /// Start exactly once; subsequent calls report an operation-state error. + ProtocolStep start(); +} + +/// Initial PIV operations; reads require an already selected PIV application. +enum PivReadOperation { select, version, algorithmConfiguration } + +/// Structured, payload-free protocol failure. Transport errors remain in Dart. +class ProtocolError { + final String kind; + final String phase; + final int? statusWord; + final String? reference; + final int? retriesRemaining; + + const ProtocolError({ + required this.kind, + required this.phase, + this.statusWord, + this.reference, + this.retriesRemaining, + }); + + @override + int get hashCode => + kind.hashCode ^ + phase.hashCode ^ + statusWord.hashCode ^ + reference.hashCode ^ + retriesRemaining.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProtocolError && + runtimeType == other.runtimeType && + kind == other.kind && + phase == other.phase && + statusWord == other.statusWord && + reference == other.reference && + retriesRemaining == other.retriesRemaining; +} + +/// Exactly one field is present: exchange a command, consume a result, or fail. +/// Command and result copies belong to Dart; getters never perform I/O. +class ProtocolStep { + final Uint8List? command; + final Uint8List? data; + final ProtocolError? error; + + const ProtocolStep({this.command, this.data, this.error}); + + @override + int get hashCode => command.hashCode ^ data.hashCode ^ error.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProtocolStep && + runtimeType == other.runtimeType && + command == other.command && + data == other.data && + error == other.error; +} diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 71a5668..0dad7ac 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -6,6 +6,7 @@ import 'api/crypto.dart'; import 'api/decode.dart'; import 'api/piv_crypto.dart'; +import 'api/protocol.dart'; import 'dart:async'; import 'dart:convert'; import 'frb_generated.dart'; @@ -68,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.13.0'; @override - int get rustContentHash => 979144991; + int get rustContentHash => -1621666059; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -80,6 +81,23 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + ProtocolStep crateApiProtocolProtocolOperationAdvance({ + required ProtocolOperation that, + required List response, + }); + + void crateApiProtocolProtocolOperationClose({ + required ProtocolOperation that, + }); + + ProtocolOperation crateApiProtocolProtocolOperationPivRead({ + required PivReadOperation kind, + }); + + ProtocolStep crateApiProtocolProtocolOperationStart({ + required ProtocolOperation that, + }); + PivPublicKeyData crateApiPivCryptoBuildPivPublicKey({ required int algorithm, required List cardData, @@ -181,6 +199,15 @@ abstract class RustLibApi extends BaseApi { required List data, required List signature, }); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_ProtocolOperation; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_ProtocolOperation; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolOperationPtr; } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -191,6 +218,130 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + ProtocolStep crateApiProtocolProtocolOperationAdvance({ + required ProtocolOperation that, + required List response, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(response, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_protocol_step, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationAdvanceConstMeta, + argValues: [that, response], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationAdvanceConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_advance", + argNames: ["that", "response"], + ); + + @override + void crateApiProtocolProtocolOperationClose({ + required ProtocolOperation that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationCloseConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_close", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationPivRead({ + required PivReadOperation kind, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_piv_read_operation(kind, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationPivReadConstMeta, + argValues: [kind], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationPivReadConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_piv_read", + argNames: ["kind"], + ); + + @override + ProtocolStep crateApiProtocolProtocolOperationStart({ + required ProtocolOperation that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_protocol_step, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationStartConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationStartConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_start", + argNames: ["that"], + ); + @override PivPublicKeyData crateApiPivCryptoBuildPivPublicKey({ required int algorithm, @@ -204,7 +355,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(cardData, serializer); sse_encode_bool(generatedResponse, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -230,7 +381,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(pngFile, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -262,7 +413,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -294,7 +445,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(certificationRequestInfo, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -326,7 +477,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(tbsCertificate, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -356,7 +507,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -378,7 +529,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -403,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(bytes, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_import_file_data, @@ -433,7 +584,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -459,7 +610,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(der, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -485,7 +636,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(pem, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -519,7 +670,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(salt, serializer); sse_encode_u_32(iterations, serializer); sse_encode_u_32(keyLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -553,7 +704,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(expectedPublicKey, serializer); sse_encode_u_8(slot, serializer); sse_encode_i_64(nowUnix, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -591,7 +742,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(country, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); sse_encode_list_String(subjectAlternativeNames, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -637,7 +788,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_opt_list_prim_u_8_strict(publicKey, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -668,7 +819,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { params, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -694,7 +845,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -717,7 +868,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -740,7 +891,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -767,7 +918,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_list_prim_u_8_loose(publicKey, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -797,7 +948,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -830,7 +981,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(publicKey, serializer); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -849,6 +1000,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["algorithm", "publicKey", "data", "signature"], ); + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_ProtocolOperation => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_ProtocolOperation => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; + + @protected + ProtocolOperation + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); + } + + @protected + ProtocolOperation + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); + } + + @protected + ProtocolOperation + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); + } + @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -867,6 +1053,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_piv_private_key_data(raw); } + @protected + ProtocolError dco_decode_box_autoadd_protocol_error(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_protocol_error(raw); + } + @protected SelfSignedCertificateParams dco_decode_box_autoadd_self_signed_certificate_params(dynamic raw) { @@ -874,12 +1066,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_self_signed_certificate_params(raw); } + @protected + int dco_decode_box_autoadd_u_16(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_box_autoadd_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + @protected X509CertData dco_decode_box_autoadd_x_509_cert_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return dco_decode_x_509_cert_data(raw); } + @protected + int dco_decode_i_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + @protected PlatformInt64 dco_decode_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -920,6 +1130,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { : dco_decode_box_autoadd_piv_private_key_data(raw); } + @protected + ProtocolError? dco_decode_opt_box_autoadd_protocol_error(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_protocol_error(raw); + } + + @protected + int? dco_decode_opt_box_autoadd_u_16(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_u_16(raw); + } + + @protected + int? dco_decode_opt_box_autoadd_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_u_8(raw); + } + @protected X509CertData? dco_decode_opt_box_autoadd_x_509_cert_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -969,6 +1197,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + PivReadOperation dco_decode_piv_read_operation(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PivReadOperation.values[raw as int]; + } + + @protected + ProtocolError dco_decode_protocol_error(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return ProtocolError( + kind: dco_decode_String(arr[0]), + phase: dco_decode_String(arr[1]), + statusWord: dco_decode_opt_box_autoadd_u_16(arr[2]), + reference: dco_decode_opt_String(arr[3]), + retriesRemaining: dco_decode_opt_box_autoadd_u_8(arr[4]), + ); + } + + @protected + ProtocolStep dco_decode_protocol_step(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return ProtocolStep( + command: dco_decode_opt_list_prim_u_8_strict(arr[0]), + data: dco_decode_opt_list_prim_u_8_strict(arr[1]), + error: dco_decode_opt_box_autoadd_protocol_error(arr[2]), + ); + } + @protected SelfSignedCertificateParams dco_decode_self_signed_certificate_params( dynamic raw, @@ -1046,6 +1308,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + ProtocolOperation + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolOperation + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolOperation + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected String sse_decode_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1067,6 +1365,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_piv_private_key_data(deserializer)); } + @protected + ProtocolError sse_decode_box_autoadd_protocol_error( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_protocol_error(deserializer)); + } + @protected SelfSignedCertificateParams sse_decode_box_autoadd_self_signed_certificate_params( @@ -1076,6 +1382,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_self_signed_certificate_params(deserializer)); } + @protected + int sse_decode_box_autoadd_u_16(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_u_16(deserializer)); + } + + @protected + int sse_decode_box_autoadd_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_u_8(deserializer)); + } + @protected X509CertData sse_decode_box_autoadd_x_509_cert_data( SseDeserializer deserializer, @@ -1084,6 +1402,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_x_509_cert_data(deserializer)); } + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1140,6 +1464,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + ProtocolError? sse_decode_opt_box_autoadd_protocol_error( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_protocol_error(deserializer)); + } else { + return null; + } + } + + @protected + int? sse_decode_opt_box_autoadd_u_16(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_u_16(deserializer)); + } else { + return null; + } + } + + @protected + int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_u_8(deserializer)); + } else { + return null; + } + } + @protected X509CertData? sse_decode_opt_box_autoadd_x_509_cert_data( SseDeserializer deserializer, @@ -1213,6 +1572,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + PivReadOperation sse_decode_piv_read_operation(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return PivReadOperation.values[inner]; + } + + @protected + ProtocolError sse_decode_protocol_error(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_kind = sse_decode_String(deserializer); + var var_phase = sse_decode_String(deserializer); + var var_statusWord = sse_decode_opt_box_autoadd_u_16(deserializer); + var var_reference = sse_decode_opt_String(deserializer); + var var_retriesRemaining = sse_decode_opt_box_autoadd_u_8(deserializer); + return ProtocolError( + kind: var_kind, + phase: var_phase, + statusWord: var_statusWord, + reference: var_reference, + retriesRemaining: var_retriesRemaining, + ); + } + + @protected + ProtocolStep sse_decode_protocol_step(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_command = sse_decode_opt_list_prim_u_8_strict(deserializer); + var var_data = sse_decode_opt_list_prim_u_8_strict(deserializer); + var var_error = sse_decode_opt_box_autoadd_protocol_error(deserializer); + return ProtocolStep(command: var_command, data: var_data, error: var_error); + } + @protected SelfSignedCertificateParams sse_decode_self_signed_certificate_params( SseDeserializer deserializer, @@ -1313,9 +1705,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - int sse_decode_i_32(SseDeserializer deserializer) { + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt32(); + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: null), + serializer, + ); } @protected @@ -1339,6 +1764,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_piv_private_key_data(self, serializer); } + @protected + void sse_encode_box_autoadd_protocol_error( + ProtocolError self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_protocol_error(self, serializer); + } + @protected void sse_encode_box_autoadd_self_signed_certificate_params( SelfSignedCertificateParams self, @@ -1348,6 +1782,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_self_signed_certificate_params(self, serializer); } + @protected + void sse_encode_box_autoadd_u_16(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_16(self, serializer); + } + + @protected + void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_8(self, serializer); + } + @protected void sse_encode_box_autoadd_x_509_cert_data( X509CertData self, @@ -1357,6 +1803,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_x_509_cert_data(self, serializer); } + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1417,6 +1869,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_protocol_error( + ProtocolError? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_protocol_error(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_u_16(int? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_u_16(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_u_8(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_x_509_cert_data( X509CertData? self, @@ -1477,6 +1962,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(self.rawPublicKey, serializer); } + @protected + void sse_encode_piv_read_operation( + PivReadOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_protocol_error(ProtocolError self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.kind, serializer); + sse_encode_String(self.phase, serializer); + sse_encode_opt_box_autoadd_u_16(self.statusWord, serializer); + sse_encode_opt_String(self.reference, serializer); + sse_encode_opt_box_autoadd_u_8(self.retriesRemaining, serializer); + } + + @protected + void sse_encode_protocol_step(ProtocolStep self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_list_prim_u_8_strict(self.command, serializer); + sse_encode_opt_list_prim_u_8_strict(self.data, serializer); + sse_encode_opt_box_autoadd_protocol_error(self.error, serializer); + } + @protected void sse_encode_self_signed_certificate_params( SelfSignedCertificateParams self, @@ -1543,10 +2055,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(self.subjectPublicKeyInfo, serializer); sse_encode_list_prim_u_8_strict(self.rawPublicKey, serializer); } +} - @protected - void sse_encode_i_32(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putInt32(self); - } +@sealed +class ProtocolOperationImpl extends RustOpaque implements ProtocolOperation { + // Not to be used by end users + ProtocolOperationImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + ProtocolOperationImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_ProtocolOperation, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_ProtocolOperation, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_ProtocolOperationPtr, + ); + + /// Consume one complete, unprocessed response including SW1/SW2. + /// The input is copied into zeroizing storage and released before returning. + ProtocolStep advance({required List response}) => RustLib.instance.api + .crateApiProtocolProtocolOperationAdvance(that: this, response: response); + + /// Release locally, including on a transport exception. Never retry/rollback. + void close() => + RustLib.instance.api.crateApiProtocolProtocolOperationClose(that: this); + + /// Start exactly once; subsequent calls report an operation-state error. + ProtocolStep start() => + RustLib.instance.api.crateApiProtocolProtocolOperationStart(that: this); } diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index 76d23b2..c2fe3b9 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -6,6 +6,7 @@ import 'api/crypto.dart'; import 'api/decode.dart'; import 'api/piv_crypto.dart'; +import 'api/protocol.dart'; import 'dart:async'; import 'dart:convert'; import 'dart:ffi' as ffi; @@ -20,6 +21,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr; + + @protected + ProtocolOperation + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + + @protected + ProtocolOperation + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + + @protected + ProtocolOperation + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); @@ -29,13 +52,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPrivateKeyData dco_decode_box_autoadd_piv_private_key_data(dynamic raw); + @protected + ProtocolError dco_decode_box_autoadd_protocol_error(dynamic raw); + @protected SelfSignedCertificateParams dco_decode_box_autoadd_self_signed_certificate_params(dynamic raw); + @protected + int dco_decode_box_autoadd_u_16(dynamic raw); + + @protected + int dco_decode_box_autoadd_u_8(dynamic raw); + @protected X509CertData dco_decode_box_autoadd_x_509_cert_data(dynamic raw); + @protected + int dco_decode_i_32(dynamic raw); + @protected PlatformInt64 dco_decode_i_64(dynamic raw); @@ -56,6 +91,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + ProtocolError? dco_decode_opt_box_autoadd_protocol_error(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_16(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_8(dynamic raw); + @protected X509CertData? dco_decode_opt_box_autoadd_x_509_cert_data(dynamic raw); @@ -71,6 +115,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPublicKeyData dco_decode_piv_public_key_data(dynamic raw); + @protected + PivReadOperation dco_decode_piv_read_operation(dynamic raw); + + @protected + ProtocolError dco_decode_protocol_error(dynamic raw); + + @protected + ProtocolStep dco_decode_protocol_step(dynamic raw); + @protected SelfSignedCertificateParams dco_decode_self_signed_certificate_params( dynamic raw, @@ -94,6 +147,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData dco_decode_x_509_cert_data(dynamic raw); + @protected + ProtocolOperation + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + + @protected + ProtocolOperation + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + + @protected + ProtocolOperation + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -105,17 +176,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolError sse_decode_box_autoadd_protocol_error( + SseDeserializer deserializer, + ); + @protected SelfSignedCertificateParams sse_decode_box_autoadd_self_signed_certificate_params( SseDeserializer deserializer, ); + @protected + int sse_decode_box_autoadd_u_16(SseDeserializer deserializer); + + @protected + int sse_decode_box_autoadd_u_8(SseDeserializer deserializer); + @protected X509CertData sse_decode_box_autoadd_x_509_cert_data( SseDeserializer deserializer, ); + @protected + int sse_decode_i_32(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); @@ -136,6 +221,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolError? sse_decode_opt_box_autoadd_protocol_error( + SseDeserializer deserializer, + ); + + @protected + int? sse_decode_opt_box_autoadd_u_16(SseDeserializer deserializer); + + @protected + int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer); + @protected X509CertData? sse_decode_opt_box_autoadd_x_509_cert_data( SseDeserializer deserializer, @@ -157,6 +253,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPublicKeyData sse_decode_piv_public_key_data(SseDeserializer deserializer); + @protected + PivReadOperation sse_decode_piv_read_operation(SseDeserializer deserializer); + + @protected + ProtocolError sse_decode_protocol_error(SseDeserializer deserializer); + + @protected + ProtocolStep sse_decode_protocol_step(SseDeserializer deserializer); + @protected SelfSignedCertificateParams sse_decode_self_signed_certificate_params( SseDeserializer deserializer, @@ -181,7 +286,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { X509CertData sse_decode_x_509_cert_data(SseDeserializer deserializer); @protected - int sse_decode_i_32(SseDeserializer deserializer); + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); @protected void sse_encode_String(String self, SseSerializer serializer); @@ -195,18 +318,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_protocol_error( + ProtocolError self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_self_signed_certificate_params( SelfSignedCertificateParams self, SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_u_16(int self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_x_509_cert_data( X509CertData self, SseSerializer serializer, ); + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); @@ -231,6 +369,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_protocol_error( + ProtocolError? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_u_16(int? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_x_509_cert_data( X509CertData? self, @@ -261,6 +411,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_piv_read_operation( + PivReadOperation self, + SseSerializer serializer, + ); + + @protected + void sse_encode_protocol_error(ProtocolError self, SseSerializer serializer); + + @protected + void sse_encode_protocol_step(ProtocolStep self, SseSerializer serializer); + @protected void sse_encode_self_signed_certificate_params( SelfSignedCertificateParams self, @@ -284,9 +446,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_x_509_cert_data(X509CertData self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); } // Section: wire_class @@ -302,4 +461,38 @@ class RustLibWire implements BaseWire { /// The symbols are looked up in [dynamicLibrary]. RustLibWire(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr + .asFunction)>(); } diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index 3dbb6e8..b0a1376 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -9,6 +9,7 @@ import 'api/crypto.dart'; import 'api/decode.dart'; import 'api/piv_crypto.dart'; +import 'api/protocol.dart'; import 'dart:async'; import 'dart:convert'; import 'frb_generated.dart'; @@ -22,6 +23,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; + + @protected + ProtocolOperation + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + + @protected + ProtocolOperation + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + + @protected + ProtocolOperation + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); @@ -31,13 +54,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPrivateKeyData dco_decode_box_autoadd_piv_private_key_data(dynamic raw); + @protected + ProtocolError dco_decode_box_autoadd_protocol_error(dynamic raw); + @protected SelfSignedCertificateParams dco_decode_box_autoadd_self_signed_certificate_params(dynamic raw); + @protected + int dco_decode_box_autoadd_u_16(dynamic raw); + + @protected + int dco_decode_box_autoadd_u_8(dynamic raw); + @protected X509CertData dco_decode_box_autoadd_x_509_cert_data(dynamic raw); + @protected + int dco_decode_i_32(dynamic raw); + @protected PlatformInt64 dco_decode_i_64(dynamic raw); @@ -58,6 +93,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + ProtocolError? dco_decode_opt_box_autoadd_protocol_error(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_16(dynamic raw); + + @protected + int? dco_decode_opt_box_autoadd_u_8(dynamic raw); + @protected X509CertData? dco_decode_opt_box_autoadd_x_509_cert_data(dynamic raw); @@ -73,6 +117,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPublicKeyData dco_decode_piv_public_key_data(dynamic raw); + @protected + PivReadOperation dco_decode_piv_read_operation(dynamic raw); + + @protected + ProtocolError dco_decode_protocol_error(dynamic raw); + + @protected + ProtocolStep dco_decode_protocol_step(dynamic raw); + @protected SelfSignedCertificateParams dco_decode_self_signed_certificate_params( dynamic raw, @@ -96,6 +149,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData dco_decode_x_509_cert_data(dynamic raw); + @protected + ProtocolOperation + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + + @protected + ProtocolOperation + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + + @protected + ProtocolOperation + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -107,17 +178,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolError sse_decode_box_autoadd_protocol_error( + SseDeserializer deserializer, + ); + @protected SelfSignedCertificateParams sse_decode_box_autoadd_self_signed_certificate_params( SseDeserializer deserializer, ); + @protected + int sse_decode_box_autoadd_u_16(SseDeserializer deserializer); + + @protected + int sse_decode_box_autoadd_u_8(SseDeserializer deserializer); + @protected X509CertData sse_decode_box_autoadd_x_509_cert_data( SseDeserializer deserializer, ); + @protected + int sse_decode_i_32(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); @@ -138,6 +223,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolError? sse_decode_opt_box_autoadd_protocol_error( + SseDeserializer deserializer, + ); + + @protected + int? sse_decode_opt_box_autoadd_u_16(SseDeserializer deserializer); + + @protected + int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer); + @protected X509CertData? sse_decode_opt_box_autoadd_x_509_cert_data( SseDeserializer deserializer, @@ -159,6 +255,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PivPublicKeyData sse_decode_piv_public_key_data(SseDeserializer deserializer); + @protected + PivReadOperation sse_decode_piv_read_operation(SseDeserializer deserializer); + + @protected + ProtocolError sse_decode_protocol_error(SseDeserializer deserializer); + + @protected + ProtocolStep sse_decode_protocol_step(SseDeserializer deserializer); + @protected SelfSignedCertificateParams sse_decode_self_signed_certificate_params( SseDeserializer deserializer, @@ -183,7 +288,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { X509CertData sse_decode_x_509_cert_data(SseDeserializer deserializer); @protected - int sse_decode_i_32(SseDeserializer deserializer); + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ); @protected void sse_encode_String(String self, SseSerializer serializer); @@ -197,18 +320,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_protocol_error( + ProtocolError self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_self_signed_certificate_params( SelfSignedCertificateParams self, SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_u_16(int self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_x_509_cert_data( X509CertData self, SseSerializer serializer, ); + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); @@ -233,6 +371,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_protocol_error( + ProtocolError? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_u_16(int? self, SseSerializer serializer); + + @protected + void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_x_509_cert_data( X509CertData? self, @@ -263,6 +413,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_piv_read_operation( + PivReadOperation self, + SseSerializer serializer, + ); + + @protected + void sse_encode_protocol_error(ProtocolError self, SseSerializer serializer); + + @protected + void sse_encode_protocol_step(ProtocolStep self, SseSerializer serializer); + @protected void sse_encode_self_signed_certificate_params( SelfSignedCertificateParams self, @@ -286,15 +448,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_x_509_cert_data(X509CertData self, SseSerializer serializer); - - @protected - void sse_encode_i_32(int self, SseSerializer serializer); } // Section: wire_class class RustLibWire implements BaseWire { RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr, + ); } @JS('wasm_bindgen') @@ -302,4 +477,14 @@ external RustLibWasmModule get wasmModule; @JS() @anonymous -extension type RustLibWasmModule._(JSObject _) implements JSObject {} +extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + int ptr, + ); +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d1ce3c5..a579298 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -42,6 +42,7 @@ dependencies = [ "cfg-if", "cipher 0.4.4", "cpufeatures 0.2.17", + "zeroize", ] [[package]] @@ -351,6 +352,101 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "canokey" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-admin", + "canokey-compat", + "canokey-oath", + "canokey-openpgp", + "canokey-piv", + "canokey-protocol", +] + +[[package]] +name = "canokey-admin" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-compat", + "canokey-protocol", +] + +[[package]] +name = "canokey-compat" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-protocol", +] + +[[package]] +name = "canokey-key" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-compat", + "canokey-protocol", + "der 0.7.10", + "pkcs1", + "spki 0.7.3", +] + +[[package]] +name = "canokey-oath" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-compat", + "canokey-protocol", + "hmac 0.12.1", + "pbkdf2 0.12.2", + "sha1 0.10.7", + "zeroize", +] + +[[package]] +name = "canokey-openpgp" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "canokey-compat", + "canokey-key", + "canokey-protocol", +] + +[[package]] +name = "canokey-piv" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "aes 0.8.4", + "canokey-compat", + "canokey-key", + "canokey-protocol", + "der 0.7.10", + "des 0.8.1", + "flate2", + "k256 0.13.4", + "p256 0.13.2", + "p384 0.13.1", + "p521 0.13.3", + "sm2", + "subtle", + "zeroize", +] + +[[package]] +name = "canokey-protocol" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +dependencies = [ + "thiserror 2.0.20", + "zeroize", +] + [[package]] name = "cbc" version = "0.1.2" @@ -430,6 +526,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.6", "inout 0.1.4", + "zeroize", ] [[package]] @@ -784,6 +881,15 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "des" version = "0.9.0" @@ -1576,6 +1682,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve 0.13.8", +] + [[package]] name = "k256" version = "0.14.0" @@ -1993,6 +2109,16 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "elliptic-curve 0.13.8", + "primeorder 0.13.6", +] + [[package]] name = "p384" version = "0.14.0" @@ -2007,6 +2133,17 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct 0.2.0", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", +] + [[package]] name = "p521" version = "0.14.0" @@ -2040,6 +2177,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "pbkdf2" version = "0.13.0" @@ -2524,9 +2671,10 @@ name = "rust_lib_canokey_console" version = "0.1.0" dependencies = [ "aes 0.9.3", + "canokey", "const-oid 0.10.2", "der 0.8.1", - "des", + "des 0.9.0", "ed25519-dalek 3.0.0", "fido2_crypto", "flutter_rust_bridge", @@ -2534,17 +2682,17 @@ dependencies = [ "hex-literal", "image", "js-sys", - "k256", + "k256 0.14.0", "log", "once_cell", "p256 0.14.0", - "p384", - "p521", - "pbkdf2", + "p384 0.14.0", + "p521 0.14.0", + "pbkdf2 0.13.0", "rand 0.10.2", "rsa", "rxing", - "sha1", + "sha1 0.11.0", "sha2 0.10.9", "sha2 0.11.0", "sm2", @@ -2725,6 +2873,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.11.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 78a8d6f..0c51620 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "10808e1760bf5efa86b5ff2e0b4acbeaba441edc" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/rust/THIRD_PARTY_LICENSES.json b/rust/THIRD_PARTY_LICENSES.json index f75f1bd..51ba6ce 100644 --- a/rust/THIRD_PARTY_LICENSES.json +++ b/rust/THIRD_PARTY_LICENSES.json @@ -233,6 +233,54 @@ "repository": "https://github.com/image-rs/byteorder-lite", "license": "Unlicense OR MIT" }, + { + "package_name": "canokey", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-admin", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-compat", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-key", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-oath", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-openpgp", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-piv", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, + { + "package_name": "canokey-protocol", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, { "package_name": "cbc", "package_version": "0.1.2", @@ -473,6 +521,12 @@ "repository": "https://github.com/jhpratt/deranged", "license": "MIT OR Apache-2.0" }, + { + "package_name": "des", + "package_version": "0.8.1", + "repository": "https://github.com/RustCrypto/block-ciphers", + "license": "MIT OR Apache-2.0" + }, { "package_name": "des", "package_version": "0.9.0", @@ -905,6 +959,12 @@ "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys", "license": "MIT OR Apache-2.0" }, + { + "package_name": "k256", + "package_version": "0.13.4", + "repository": "https://github.com/RustCrypto/elliptic-curves/tree/master/k256", + "license": "Apache-2.0 OR MIT" + }, { "package_name": "k256", "package_version": "0.14.0", @@ -1163,12 +1223,24 @@ "repository": "https://github.com/RustCrypto/elliptic-curves", "license": "Apache-2.0 OR MIT" }, + { + "package_name": "p384", + "package_version": "0.13.1", + "repository": "https://github.com/RustCrypto/elliptic-curves/tree/master/p384", + "license": "Apache-2.0 OR MIT" + }, { "package_name": "p384", "package_version": "0.14.0", "repository": "https://github.com/RustCrypto/elliptic-curves", "license": "Apache-2.0 OR MIT" }, + { + "package_name": "p521", + "package_version": "0.13.3", + "repository": "https://github.com/RustCrypto/elliptic-curves/tree/master/p521", + "license": "Apache-2.0 OR MIT" + }, { "package_name": "p521", "package_version": "0.14.0", @@ -1187,6 +1259,12 @@ "repository": "https://github.com/dtolnay/paste", "license": "MIT OR Apache-2.0" }, + { + "package_name": "pbkdf2", + "package_version": "0.12.2", + "repository": "https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2", + "license": "MIT OR Apache-2.0" + }, { "package_name": "pbkdf2", "package_version": "0.13.0", @@ -1553,6 +1631,12 @@ "repository": "https://github.com/RustCrypto/formats", "license": "Apache-2.0 OR MIT" }, + { + "package_name": "sha1", + "package_version": "0.10.7", + "repository": "https://github.com/RustCrypto/hashes", + "license": "MIT OR Apache-2.0" + }, { "package_name": "sha1", "package_version": "0.11.0", diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 4a594de..9e33a0e 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -1,6 +1,7 @@ pub mod crypto; pub mod decode; pub mod piv_crypto; +pub mod protocol; #[flutter_rust_bridge::frb(init)] pub fn init_app() { diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs new file mode 100644 index 0000000..4c0b955 --- /dev/null +++ b/rust/src/api/protocol.rs @@ -0,0 +1,224 @@ +//! Owned libcanokey operations. Dart owns the selected connection and every I/O. +use std::sync::Mutex; + +use canokey::{piv, Error, ErrorKind, Operation, OperationOptions, SecretBytes, Step}; + +/// Initial PIV operations; reads require an already selected PIV application. +#[derive(Clone, Copy)] +pub enum PivReadOperation { + Select, + Version, + AlgorithmConfiguration, +} + +/// Structured, payload-free protocol failure. Transport errors remain in Dart. +pub struct ProtocolError { + pub kind: String, + pub phase: String, + pub status_word: Option, + pub reference: Option, + pub retries_remaining: Option, +} + +impl From for ProtocolError { + fn from(error: Error) -> Self { + Self { + kind: format!("{:?}", error.kind), + phase: format!("{:?}", error.phase), + status_word: error.status_word.map(|sw| sw.raw()), + reference: error.reference.map(|reference| format!("{reference:?}")), + retries_remaining: error.retries_remaining, + } + } +} + +/// Exactly one field is present: exchange a command, consume a result, or fail. +/// Command and result copies belong to Dart; getters never perform I/O. +pub struct ProtocolStep { + pub command: Option>, + pub data: Option>, + pub error: Option, +} + +impl ProtocolStep { + fn failure(error: Error) -> Self { + Self { + command: None, + data: None, + error: Some(error.into()), + } + } +} + +enum Inner { + Bytes(Operation), + Configuration(Operation), +} + +/// A single caller-owned operation, retained across Dart await points. +/// Close is idempotent and releases Rust buffers without sending any APDU. +#[flutter_rust_bridge::frb(opaque)] +pub struct ProtocolOperation { + // FRB opaque handles require Sync, while core operations are only Send. + // All entry points take &mut self; get_mut never locks or spans Dart I/O. + inner: Mutex>, + construction_error: Option, +} + +impl ProtocolOperation { + /// Construct without I/O. Only Select changes applet selection; reads never + /// probe, SELECT, authenticate, or retain a device profile implicitly. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_read(kind: PivReadOperation) -> Self { + let options = OperationOptions::default(); + let operation = match kind { + PivReadOperation::Select => piv::select_application(options).map(Inner::Bytes), + PivReadOperation::Version => piv::read_version_selected(options).map(Inner::Bytes), + PivReadOperation::AlgorithmConfiguration => { + piv::read_configuration_selected(options).map(Inner::Configuration) + } + }; + match operation { + Ok(inner) => Self { + inner: Mutex::new(Some(inner)), + construction_error: None, + }, + Err(error) => Self { + inner: Mutex::new(None), + construction_error: Some(error), + }, + } + } + + /// Start exactly once; subsequent calls report an operation-state error. + #[flutter_rust_bridge::frb(sync)] + pub fn start(&mut self) -> ProtocolStep { + self.drive(None) + } + + /// Consume one complete, unprocessed response including SW1/SW2. + /// The input is copied into zeroizing storage and released before returning. + #[flutter_rust_bridge::frb(sync)] + pub fn advance(&mut self, response: Vec) -> ProtocolStep { + let response = SecretBytes::new(response); + self.drive(Some(response.as_bytes())) + } + + /// Release locally, including on a transport exception. Never retry/rollback. + #[flutter_rust_bridge::frb(sync)] + pub fn close(&mut self) { + self.inner + .get_mut() + .unwrap_or_else(|e| e.into_inner()) + .take(); + self.construction_error.take(); + } +} + +impl ProtocolOperation { + fn drive(&mut self, response: Option<&[u8]>) -> ProtocolStep { + if let Some(error) = self.construction_error.take() { + return ProtocolStep::failure(error); + } + let result = match self + .inner + .get_mut() + .unwrap_or_else(|e| e.into_inner()) + .as_mut() + { + Some(Inner::Bytes(op)) => drive(op, response, |data| data.as_bytes().to_vec()), + Some(Inner::Configuration(op)) => drive(op, response, |data| data.raw().to_vec()), + None => Err(Error::new(ErrorKind::OperationStateError)), + }; + result.unwrap_or_else(ProtocolStep::failure) + } +} + +fn drive( + op: &mut Operation, + response: Option<&[u8]>, + encode: impl FnOnce(T) -> Vec, +) -> Result { + let step = match response { + Some(response) => op.advance(response)?, + None => op.start()?, + }; + Ok(match step { + Step::Exchange => ProtocolStep { + command: Some(op.command()?.as_bytes().to_vec()), + data: None, + error: None, + }, + Step::Done => ProtocolStep { + command: None, + data: Some(encode(op.take_result()?)), + error: None, + }, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn selected_version_continuation_and_le_correction() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); + assert_eq!(op.start().command.unwrap(), [0, 0xfd, 0, 0, 0]); + assert_eq!( + op.advance(vec![0x6c, 3]).command.unwrap(), + [0, 0xfd, 0, 0, 3] + ); + assert_eq!( + op.advance(vec![6, 0x61, 2]).command.unwrap(), + [0, 0xc0, 0, 0, 2] + ); + assert_eq!(op.advance(vec![0, 0, 0x90, 0]).data.unwrap(), [6, 0, 0]); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + op.close(); + op.close(); + assert_eq!( + op.advance(vec![0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + } + + #[test] + fn select_and_structured_failure() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Select); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xa0, 0, 0, 3, 8, 0] + ); + let error = op.advance(vec![0x6a, 0x82]).error.unwrap(); + assert_eq!(error.kind, "UnsupportedDevice"); + assert_eq!(error.phase, "Select"); + assert_eq!(error.status_word, Some(0x6a82)); + } + + #[test] + fn malformed_and_oversized_responses_are_terminal() { + for response in [vec![0x90], vec![0, 0x90, 0], vec![0; 259]] { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); + op.start(); + assert!(op.advance(response).error.is_some()); + assert_eq!( + op.advance(vec![6, 0, 0, 0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + } + } + + #[test] + fn configuration_uses_upstream_validation() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + assert_eq!(op.start().command.unwrap(), [0, 0xee, 1, 0, 0]); + assert!(op.advance(vec![1, 0x90, 0]).error.is_some()); + let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + op.start(); + let bytes = vec![1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3]; + let mut response = bytes.clone(); + response.extend([0x90, 0]); + assert_eq!(op.advance(response).data.unwrap(), bytes); + } +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 427e1c3..61959f5 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -27,6 +27,7 @@ // Section: imports +use crate::api::protocol::*; use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; use flutter_rust_bridge::{Handler, IntoIntoDart}; @@ -39,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 979144991; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1621666059; // Section: executor @@ -47,6 +48,183 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__api__protocol__ProtocolOperation_advance_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_advance", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_response = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::advance( + &mut *api_that_guard, + api_response, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolOperation_close_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_close", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::protocol::ProtocolOperation::close(&mut *api_that_guard); + })?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolOperation_piv_read_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_piv_read", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_kind = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::piv_read(api_kind))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolOperation_start_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_start", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::start( + &mut *api_that_guard, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__piv_crypto__build_piv_public_key_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -786,8 +964,34 @@ fn wire__crate__api__crypto__verify_piv_signature_impl( ) } +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + // Section: dart2rust +impl SseDecode for ProtocolOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -803,6 +1007,13 @@ impl SseDecode for bool { } } +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + impl SseDecode for i64 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -858,6 +1069,41 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -920,6 +1166,51 @@ impl SseDecode for crate::api::piv_crypto::PivPublicKeyData { } } +impl SseDecode for crate::api::protocol::PivReadOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::PivReadOperation::Select, + 1 => crate::api::protocol::PivReadOperation::Version, + 2 => crate::api::protocol::PivReadOperation::AlgorithmConfiguration, + _ => unreachable!("Invalid variant for PivReadOperation: {}", inner), + }; + } +} + +impl SseDecode for crate::api::protocol::ProtocolError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_kind = ::sse_decode(deserializer); + let mut var_phase = ::sse_decode(deserializer); + let mut var_statusWord = >::sse_decode(deserializer); + let mut var_reference = >::sse_decode(deserializer); + let mut var_retriesRemaining = >::sse_decode(deserializer); + return crate::api::protocol::ProtocolError { + kind: var_kind, + phase: var_phase, + status_word: var_statusWord, + reference: var_reference, + retries_remaining: var_retriesRemaining, + }; + } +} + +impl SseDecode for crate::api::protocol::ProtocolStep { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_command = >>::sse_decode(deserializer); + let mut var_data = >>::sse_decode(deserializer); + let mut var_error = >::sse_decode(deserializer); + return crate::api::protocol::ProtocolStep { + command: var_command, + data: var_data, + error: var_error, + }; + } +} + impl SseDecode for crate::api::piv_crypto::SelfSignedCertificateParams { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1019,13 +1310,6 @@ impl SseDecode for crate::api::crypto::X509CertData { } } -impl SseDecode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i32::().unwrap() - } -} - fn pde_ffi_dispatcher_primary_impl( func_id: i32, port: flutter_rust_bridge::for_generated::MessagePort, @@ -1047,58 +1331,81 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 1 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), - 2 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), - 3 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( + 1 => { + wire__crate__api__protocol__ProtocolOperation_advance_impl(ptr, rust_vec_len, data_len) + } + 2 => wire__crate__api__protocol__ProtocolOperation_close_impl(ptr, rust_vec_len, data_len), + 3 => { + wire__crate__api__protocol__ProtocolOperation_piv_read_impl(ptr, rust_vec_len, data_len) + } + 4 => wire__crate__api__protocol__ProtocolOperation_start_impl(ptr, rust_vec_len, data_len), + 5 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), + 6 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), + 7 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( ptr, rust_vec_len, data_len, ), - 4 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( + 8 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), + 9 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 6 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), - 8 => wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( + 10 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), + 11 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), + 12 => wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len), + 13 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( ptr, rust_vec_len, data_len, ), - 10 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( + 14 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), + 16 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), + 17 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( ptr, rust_vec_len, data_len, ), - 14 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( + 18 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), + 19 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( ptr, rust_vec_len, data_len, ), - 16 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( + 20 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 17 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), - 18 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), - 20 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), - 21 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), - 22 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), + 21 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), + 22 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), + 23 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), + 24 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), + 25 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), + 26 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } // Section: rust2dart +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for ProtocolOperation { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::piv_crypto::PivImportFileData { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -1164,6 +1471,74 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::PivReadOperation { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Select => 0.into_dart(), + Self::Version => 1.into_dart(), + Self::AlgorithmConfiguration => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::PivReadOperation +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::PivReadOperation +{ + fn into_into_dart(self) -> crate::api::protocol::PivReadOperation { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::ProtocolError { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.kind.into_into_dart().into_dart(), + self.phase.into_into_dart().into_dart(), + self.status_word.into_into_dart().into_dart(), + self.reference.into_into_dart().into_dart(), + self.retries_remaining.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::ProtocolError +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::ProtocolError +{ + fn into_into_dart(self) -> crate::api::protocol::ProtocolError { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::ProtocolStep { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.command.into_into_dart().into_dart(), + self.data.into_into_dart().into_dart(), + self.error.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::ProtocolStep +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::ProtocolStep +{ + fn into_into_dart(self) -> crate::api::protocol::ProtocolStep { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::piv_crypto::SelfSignedCertificateParams { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1227,6 +1602,24 @@ impl flutter_rust_bridge::IntoIntoDart } } +impl SseEncode for ProtocolOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1241,6 +1634,13 @@ impl SseEncode for bool { } } +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + impl SseEncode for i64 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1288,6 +1688,36 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1336,6 +1766,43 @@ impl SseEncode for crate::api::piv_crypto::PivPublicKeyData { } } +impl SseEncode for crate::api::protocol::PivReadOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::PivReadOperation::Select => 0, + crate::api::protocol::PivReadOperation::Version => 1, + crate::api::protocol::PivReadOperation::AlgorithmConfiguration => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::api::protocol::ProtocolError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.kind, serializer); + ::sse_encode(self.phase, serializer); + >::sse_encode(self.status_word, serializer); + >::sse_encode(self.reference, serializer); + >::sse_encode(self.retries_remaining, serializer); + } +} + +impl SseEncode for crate::api::protocol::ProtocolStep { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(self.command, serializer); + >>::sse_encode(self.data, serializer); + >::sse_encode(self.error, serializer); + } +} + impl SseEncode for crate::api::piv_crypto::SelfSignedCertificateParams { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1409,13 +1876,6 @@ impl SseEncode for crate::api::crypto::X509CertData { } } -impl SseEncode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_i32::(self).unwrap(); - } -} - #[cfg(not(target_family = "wasm"))] mod io { // This file is automatically generated, so please do not edit it. @@ -1424,6 +1884,7 @@ mod io { // Section: imports use super::*; + use crate::api::protocol::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1433,6 +1894,20 @@ mod io { // Section: boilerplate flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } } #[cfg(not(target_family = "wasm"))] pub use io::*; @@ -1446,6 +1921,7 @@ mod web { // Section: imports use super::*; + use crate::api::protocol::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; @@ -1457,6 +1933,20 @@ mod web { // Section: boilerplate flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } } #[cfg(target_family = "wasm")] pub use web::*; diff --git a/test/controller/applets/piv/piv_certificate_loading_test.dart b/test/controller/applets/piv/piv_certificate_loading_test.dart index e03faf1..3608754 100644 --- a/test/controller/applets/piv/piv_certificate_loading_test.dart +++ b/test/controller/applets/piv/piv_certificate_loading_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:canokey_console/src/rust/api/crypto.dart'; import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:canokey_console/controller/applets/piv/piv_controller.dart'; @@ -45,7 +47,16 @@ void main() { addTearDown(RustLib.dispose); final transport = CertificateTransport(['9000', '530570033001009000']); final controller = PivController( - client: PivCardClient(transport: transport), + client: PivCardClient( + transport: transport, + readExecutor: (kind) async { + expect(kind, PivReadOperation.select); + SmartCard.assertOK( + await transport.transceive('00A4040005A00000030800'), + ); + return Uint8List(0); + }, + ), ); controller.certificateSlots.add(0x9a); @@ -59,7 +70,7 @@ void main() { expect(controller.publicKeyDerForSlot(0x9d), isNull); expect(controller.slots, isEmpty); expect(transport.commands, [ - '00A4040005A000000308', + '00A4040005A00000030800', '00CB3FFF055C035FC10500', ]); }, @@ -79,7 +90,16 @@ void main() { '6A82', ]); final controller = PivController( - client: PivCardClient(transport: transport), + client: PivCardClient( + transport: transport, + readExecutor: (kind) async { + expect(kind, PivReadOperation.select); + SmartCard.assertOK( + await transport.transceive('00A4040005A00000030800'), + ); + return Uint8List(0); + }, + ), ); controller.certificateSlots.addAll([0x9a, 0x9d]); @@ -90,9 +110,9 @@ void main() { await controller.loadSlotDetails(0x9d); expect(transport.responses, isEmpty); expect(transport.commands, [ - '00A4040005A000000308', + '00A4040005A00000030800', '00CB3FFF055C035FC10500', - '00A4040005A000000308', + '00A4040005A00000030800', '00CB3FFF055C035FC10b00', ]); } @@ -105,7 +125,16 @@ void main() { addTearDown(RustLib.dispose); final transport = CertificateTransport(['9000', '530570033001009000']); final controller = PivController( - client: PivCardClient(transport: transport), + client: PivCardClient( + transport: transport, + readExecutor: (kind) async { + expect(kind, PivReadOperation.select); + SmartCard.assertOK( + await transport.transceive('00A4040005A00000030800'), + ); + return Uint8List(0); + }, + ), ); controller.certificateSlots.add(0x9a); diff --git a/test/helper/utils/piv_card_test.dart b/test/helper/utils/piv_card_test.dart index d1abb82..d27f515 100644 --- a/test/helper/utils/piv_card_test.dart +++ b/test/helper/utils/piv_card_test.dart @@ -1,4 +1,9 @@ +@Tags(['native']) +library; + import 'dart:io'; +import 'package:canokey_console/src/rust/frb_generated.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:convert/convert.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/piv_card.dart'; @@ -6,6 +11,8 @@ import 'package:canokey_console/models/piv.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); test( 'empty VERIFY reads remaining attempts without guessing on success', () async { @@ -178,7 +185,7 @@ void main() { expect(metadata!.retriesCount, 3); expect(metadata.remainingCount, 3); expect(transport.commands, [ - '00A4040005A000000308', + '00A4040005A00000030800', '00FD000000', '00F8000000', '0020008000', @@ -235,6 +242,33 @@ void main() { }, ); + test( + 'libcanokey version handles continuation without an extra SELECT', + () async { + final transport = _QueueApduTransport(['6C03', '066102', '00009000']); + expect(await PivCardClient(transport: transport).readVersion(), [ + 6, + 0, + 0, + ]); + expect(transport.commands, ['00FD000000', '00FD000003', '00C0000002']); + }, + ); + + test( + 'algorithm configuration propagates security and malformed replies', + () async { + for (final response in ['6982', '6F00', '019000']) { + final transport = _QueueApduTransport(['9000', response]); + await expectLater( + PivCardClient(transport: transport).readAlgorithmExtensions(), + throwsA(isA()), + ); + expect(transport.commands, ['00A4040005A00000030800', '00EE010000']); + } + }, + ); + test('reads optional algorithm extensions and missing metadata', () async { final config = await PivCardClient( transport: _QueueApduTransport(['9000', '01E00516E1531554E2E39000']), diff --git a/test/helper/utils/protocol_operation_test.dart b/test/helper/utils/protocol_operation_test.dart new file mode 100644 index 0000000..b1cbd30 --- /dev/null +++ b/test/helper/utils/protocol_operation_test.dart @@ -0,0 +1,135 @@ +import 'dart:typed_data'; + +import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test( + 'passes complete responses and wipes exchange copies before closing', + () async { + final command = Uint8List.fromList([0, 0xfd, 0, 0, 0]); + final data = Uint8List.fromList([6, 0, 0]); + final operation = _Operation([ + ProtocolStep(command: command), + ProtocolStep(data: data), + ]); + final transport = _Transport((command) async { + expect(command, '00FD000000'); + return '0600009000'; + }); + expect(await executeProtocolOperation(operation, transport), data); + expect(operation.responses, [ + [6, 0, 0, 0x90, 0], + ]); + expect(command, everyElement(0)); + expect(operation.borrowedResponse, everyElement(0)); + expect(operation.closed, isTrue); + expect(operation.disposed, isTrue); + expect(data, [6, 0, 0]); + }, + ); + + test( + 'transport failure propagates unchanged, closes, and never retries', + () async { + final failure = StateError('disconnected'); + final command = Uint8List.fromList([0, 0xfd, 0, 0, 0]); + final operation = _Operation([ProtocolStep(command: command)]); + var calls = 0; + await expectLater( + executeProtocolOperation( + operation, + _Transport((_) async { + calls++; + throw failure; + }), + ), + throwsA(same(failure)), + ); + expect(calls, 1); + expect(operation.responses, isEmpty); + expect(command, everyElement(0)); + expect(operation.closed && operation.disposed, isTrue); + }, + ); + + test('malformed transport hex closes without advancing', () async { + final operation = _Operation([ProtocolStep(command: Uint8List(5))]); + await expectLater( + executeProtocolOperation(operation, _Transport((_) async => 'xyz')), + throwsFormatException, + ); + expect(operation.responses, isEmpty); + expect(operation.closed && operation.disposed, isTrue); + }); + + test('protocol error retains all structured details and closes', () async { + const details = ProtocolError( + kind: 'AuthenticationFailed', + phase: 'Authentication', + statusWord: 0x63c2, + reference: 'Pin', + retriesRemaining: 2, + ); + final operation = _Operation([const ProtocolStep(error: details)]); + await expectLater( + executeProtocolOperation( + operation, + _Transport((_) async { + fail('No exchange after failure'); + }), + ), + throwsA( + isA().having( + (error) => error.details, + 'details', + same(details), + ), + ), + ); + expect(operation.closed && operation.disposed, isTrue); + }); + + test('unexpected bridge exception still releases the operation', () async { + final operation = _Operation([]); + await expectLater( + executeProtocolOperation(operation, _Transport((_) async => '9000')), + throwsRangeError, + ); + expect(operation.closed && operation.disposed, isTrue); + }); +} + +class _Operation implements ProtocolOperation { + _Operation(this.steps); + final List steps; + final responses = >[]; + Uint8List? borrowedResponse; + bool closed = false; + bool disposed = false; + + @override + ProtocolStep start() => steps.removeAt(0); + @override + ProtocolStep advance({required List response}) { + responses.add(List.of(response)); + borrowedResponse = response as Uint8List; + return steps.removeAt(0); + } + + @override + void close() => closed = true; + @override + void dispose() => disposed = true; + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _Transport implements ApduTransport { + _Transport(this.exchange); + final Future Function(String) exchange; + @override + Future transceive(String capdu) => exchange(capdu); +} diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index e252c6c..d198880 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; @@ -1019,6 +1020,8 @@ void main() { } Future _runSmoke() async { + await RustLib.init(); + addTearDown(RustLib.dispose); await initializeFido2Backend(); final environment = Platform.environment; _expect(environment['CANOKEY_USBIP'] == '1', 'CANOKEY_USBIP must be 1'); From 6620bfca75dd595cb9a73a40cb12bb25be764ccf Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 14:13:26 +0800 Subject: [PATCH 02/19] feat: route all applet protocol operations through libcanokey Bump libcanokey to 95e1930e and complete the migration: PIV PQ seed import, OATH set-default, NDEF, Pass and the CTAP transport now use upstream operations via new facade bindings, alongside the previously migrated PIV/OpenPGP/Admin paths. No Dart code builds APDUs by hand anymore (CBOR/ClientPin stays host-side by design). Also converge repeated Admin SELECT/VERIFY onto admin::Access::Existing behind lease-bound session evidence, and cover all native-tagged tests in the USB/IP workflow via --tags native. --- .github/workflows/usbip.yml | 5 +- README.md | 22 +- docs/libcanokey-migration.md | 456 +- .../applets/oath/oath_controller.dart | 335 +- .../applets/openpgp/openpgp_controller.dart | 8 + .../applets/pass/pass_controller.dart | 41 +- .../applets/piv/piv_controller.dart | 1151 +-- .../applets/settings/settings_controller.dart | 364 +- .../applets/webauthn/webauthn_controller.dart | 34 +- lib/controller/base/admin.dart | 103 +- lib/controller/base/polling_controller.dart | 42 +- lib/helper/storage/local_storage.dart | 58 +- lib/helper/utils/admin_card.dart | 470 +- lib/helper/utils/apdu_transport.dart | 4 +- lib/helper/utils/applet_switches.dart | 2 +- lib/helper/utils/card_session.dart | 244 + lib/helper/utils/ctap_transmitter.dart | 76 +- lib/helper/utils/ndef_card.dart | 169 +- lib/helper/utils/oath_card.dart | 442 +- lib/helper/utils/openpgp_card.dart | 404 +- lib/helper/utils/pass_card.dart | 284 +- lib/helper/utils/piv_card.dart | 688 +- lib/helper/utils/piv_management_key.dart | 83 - lib/helper/utils/piv_post_quantum.dart | 77 - lib/helper/utils/piv_signature.dart | 21 - lib/helper/utils/protocol_operation.dart | 122 +- lib/helper/utils/smartcard.dart | 329 +- lib/helper/widgets/input_pin_dialog.dart | 29 +- lib/models/pass.dart | 110 +- lib/models/piv.dart | 34 +- lib/models/webauthn.dart | 24 +- lib/src/rust/api/protocol.dart | 638 +- lib/src/rust/frb_generated.dart | 5281 +++++++++++++- lib/src/rust/frb_generated.io.dart | 335 + lib/src/rust/frb_generated.web.dart | 327 + .../pass/dialogs/slot_config_dialog.dart | 2 + lib/views/applets/pass/widgets/slot_card.dart | 1 + lib/views/applets/settings/settings_page.dart | 22 + .../dialogs/force_pin_change_dialog.dart | 13 +- rust/Cargo.lock | 35 +- rust/Cargo.toml | 3 +- rust/src/api/decode.rs | 5 +- rust/src/api/protocol.rs | 3857 +++++++++- rust/src/frb_generated.rs | 6319 +++++++++++++++-- .../piv/piv_certificate_loading_test.dart | 55 +- .../settings/settings_configuration_test.dart | 9 +- .../webauthn/ctap_transmitter_test.dart | 106 +- .../helper/storage/credential_cache_test.dart | 58 + test/helper/utils/admin_card_test.dart | 766 +- test/helper/utils/applet_switches_test.dart | 33 +- test/helper/utils/card_session_test.dart | 249 + test/helper/utils/ndef_card_test.dart | 277 +- test/helper/utils/oath_card_test.dart | 397 +- test/helper/utils/openpgp_card_test.dart | 439 +- test/helper/utils/pass_card_test.dart | 380 +- test/helper/utils/piv_card_test.dart | 878 ++- .../helper/utils/piv_management_key_test.dart | 113 - test/helper/utils/piv_post_quantum_test.dart | 81 - test/helper/utils/piv_signature_test.dart | 62 - .../helper/utils/protocol_operation_test.dart | 135 +- test/usbip/console_smoke.dart | 755 +- .../applets/settings/settings_page_test.dart | 36 + 62 files changed, 23883 insertions(+), 4015 deletions(-) create mode 100644 lib/helper/utils/card_session.dart delete mode 100644 lib/helper/utils/piv_management_key.dart delete mode 100644 lib/helper/utils/piv_post_quantum.dart create mode 100644 test/helper/storage/credential_cache_test.dart create mode 100644 test/helper/utils/card_session_test.dart delete mode 100644 test/helper/utils/piv_management_key_test.dart delete mode 100644 test/helper/utils/piv_post_quantum_test.dart delete mode 100644 test/helper/utils/piv_signature_test.dart diff --git a/.github/workflows/usbip.yml b/.github/workflows/usbip.yml index e005ffa..cc1b71f 100644 --- a/.github/workflows/usbip.yml +++ b/.github/workflows/usbip.yml @@ -82,10 +82,7 @@ jobs: env: FIDO2_CRYPTO_LIBRARY: ${{ github.workspace }}/build/usbip-backend/librust_lib_canokey_console.so FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR: ${{ github.workspace }}/build/usbip-backend - run: >- - flutter test --no-pub - test/helper/utils/fido2_backend_test.dart - test/helper/utils/piv_card_test.dart + run: flutter test --no-pub --tags native - name: Share native backend with firmware jobs uses: actions/upload-artifact@v7 diff --git a/README.md b/README.md index c52ca02..9adaa09 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,30 @@ Visit our web application at [CanoKey Console Web](https://console.canokeys.org) 3. Run the application: ```bash - flutter_rust_bridge_codegen build-web --release # for web only, remove --release for debug Rust build (very slow when decoding qrcode!) + flutter_rust_bridge_codegen build-web --release --wasm-pack-rustup-toolchain nightly-2026-09-04 # for web only dart run fido2:setup --web --output=build/fido2 # for web only mkdir -p web/fido2 && cp build/fido2/web/fido2_crypto* web/fido2/ # for web only flutter run ``` +For Chrome development, run `flutter run -d chrome --cross-origin-isolation` +to enable the headers required for shared WASM memory. + +If `wasm-pack` cannot download its helper and fails while compiling +`wasm-bindgen-cli` with WASM linker flags on the host, install the matching +helper separately with `cargo +stable install wasm-bindgen-cli --version 0.2.100 --locked` +and ensure its `bin` directory is on `PATH` before rebuilding. + +After changing Rust code or regenerating Flutter Rust Bridge bindings, rebuild +the web bundle with the `build-web` command above, then stop and relaunch the +Flutter app. Hot restart does not recompile Rust. A content-hash mismatch means +the loaded WASM bundle and generated Dart bindings are out of sync. + +If hot restart reports `Identifier 'wasm_bindgen' has already been declared`, +fully reload the browser page or stop and relaunch the app. The bridge's web +loader inserts its script again on hot restart, while the previous script's +global declaration remains in the page. + ## Diagnostic Logs Open **Settings > View Logs** to inspect this session's local logs without a @@ -101,7 +119,7 @@ flutter test test/helper/utils/fido2_backend_test.dart ### Web ```bash -flutter_rust_bridge_codegen build-web --release +flutter_rust_bridge_codegen build-web --release --wasm-pack-rustup-toolchain nightly-2026-09-04 dart run fido2:setup --web --output=build/fido2 mkdir -p web/fido2 && cp build/fido2/web/fido2_crypto* web/fido2/ flutter build web diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 911bf90..3ca0bd1 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -2,7 +2,7 @@ The first increment uses the Rust facade through Flutter Rust Bridge, following [canokey-pkcs11#5](https://github.com/canokeys/canokey-pkcs11/pull/5). Cargo pins -libcanokey to the same `10808e1760bf5efa86b5ff2e0b4acbeaba441edc` revision. +libcanokey to the same `95e1930ea86eb248d0295bce9880cee5c65fd85e` revision. The C ABI is unnecessary in Console's existing Rust library. ## Implemented @@ -11,40 +11,309 @@ The C ABI is unnecessary in Console's existing Rust library. readers. This is still an explicit caller action and may clear authentication. - PIV version and algorithm configuration reads use the upstream selected-app operations. Neither inserts SELECT, probes the device, or authenticates. +- PIV certificate reads use `read_certificate` with `Access::Existing` and an + explicitly prepared, lease-owned profile. The former local certificate machine + has been removed. libcanokey owns GET DATA, container validation and bounded gzip + decompression; no SELECT/probe/authentication is inserted. Only 6A82/6A88 return + a missing object; malformed certificates remain visible as invalid UI slots. +- PIV management-protection object reads/writes use `read_object`/`write_object` + with `Access::Existing`. The controller no longer builds GET/PUT DATA or unwraps + outer 53 containers. Missing objects alone become null; malformed/security + failures propagate. Upstream owns firmware checks, framing and segmentation. + Writes are never resumed or rolled back after partial or uncertain outcomes. - An opaque FRB operation owns its protocol state across Dart await points. The Dart executor exchanges complete APDUs, including response status words. libcanokey owns continuation, safe Le correction, budgets and validation. - Completion, protocol failures, malformed transport hex and transport exceptions all close/dispose the operation. No mutation is retried or rolled back. Byte buffers copied to Dart are wiped after each exchange; immutable transport - strings and existing diagnostic logs are not erasable by this executor. + strings are not erasable by this executor. Console's physical transport retains + the existing full `C-APDU`/`R-APDU` diagnostic logs, including for lease-based + operations. These diagnostic copies are not erased by operation cleanup. +- Explicit `prepare()` probes Admin/PIV before metadata workflows authenticate. + The resulting opaque profile belongs to that physical lease and is closed on + rebinding or lease release. Once re-probing starts, failure cannot leave an older + profile usable; competing preparation is rejected before touching an active + operation's profile. +- PIV key/PIN/PUK/management metadata now uses `get_metadata` with `Access::Existing` and + upstream capability/slot checks, continuation and field validation. Reads never + SELECT, probe or authenticate. Only upstream `NotFound` (including supported + legacy 6900 empty-key responses) becomes null; security, unsupported-command, + invalid-length and malformed-field errors remain visible. UI mapping rejects + incomplete/unknown required display fields instead of inventing retries or + policies. Key algorithm display resolves through this device's profile, ignoring + stale UI extension IDs; display enum IDs never authorize a device operation. +- PIN verification/change, PUK change, PIN unblocking and explicit logout now use + `credential(select=false)`. Inputs enter upstream zeroizing owners; Dart wipes its + mutable input copies. Credential validation and legacy explicit Le follow the + pinned library. PIN/PUK failures preserve their reference and remaining retries; + only authentication rejection/blocked credentials map to the UI's false result. + Successful credential operations have `Unchanged` profile effect and do not + create a reusable authorization token. +- Management-key external authentication now uses + `authenticate_management_key(select=false)`: libcanokey owns both challenge APDUs, + parsing and 3DES/AES cryptography, with current-profile capability checks. + The old Dart challenge builders/parser were removed. This preserves external + mode; it does not claim mutual device authentication. +- Selected PIN status uses upstream `command::pin_status`, the operation engine + and error classifier. A small adapter retains the validated status word because + the pinned high-level `get_pin_status` inserts SELECT, which would disturb an + already authenticated transaction. Unexpected statuses and nonempty replies are + errors; 9000 never invents a retry count. Serial reads now return the optional + Admin serial observed by the explicit probe, without another applet switch. +- Explicit PUK blocking retains Console's bounded, intentional destructive loop, + with each CHANGE PUK executed upstream. Only expected credential outcomes allow + the next attempt; uncertain I/O, malformed responses and unexpected statuses + stop immediately. No mutation is replayed after a 6C response. +- Each executor reserves its lease across the entire operation, preventing raw + exchanges, another operation or rebind between continuation APDUs. - Errors retain category, phase, status word, credential reference and retry count as separate fields. Transport errors are propagated without conversion. - Only an unsupported algorithm-configuration instruction permits the existing firmware defaults. Security failures, malformed responses and communication errors no longer silently choose default algorithms. +- OpenPGP card-info reads, PW1/PW3 credential operations and all + administrative writes (reset code set/clear, retry limits, signature PIN + policy, touch policy/cache time, admin and reset-code unblocks) use upstream + `openpgp::operation` through the bridge. The client prepares with the minimal + Admin probe; the profile is immutable firmware evidence, and every upstream + operation SELECTs OpenPGP and explicitly verifies its own password. Only + `NotFound` makes optional cardholder/URL/touch-cache objects absent; the + pre-UIF firmware gate also stays absent without performing I/O. Credential + rejections, blocked passwords and target-stage security rejections map to + the UI's false result with the retained status word; other protocol and + transport failures propagate. `ResetRetries` resets PW1/PW3 to firmware + defaults on the card; Console caches no OpenPGP credentials, so no local + credential invalidation is required. +- OATH selection, access-code validation, put/delete/calculate/calculate-all + and code set/clear use upstream `oath::operation` through the bridge. The + FRB wrappers carry an optional access key plus fresh host challenge; each + operation SELECTs and validates within itself, so no authentication state is + cached or reused between operations. The controller's per-use-case + authentication only proves the code for prompting/cache policy. Legacy + (1.3.x) versus modern dialects come from the probed firmware profile, not + from parsing SELECT response TLVs; a legacy selection yields no synthetic + version or salt. Calculation results keep names, digit counts, raw truncated + bytes and HOTP/touch markers so display formatting (decimal, Steam) stays in + Dart. Wrong codes map `AuthenticationFailed`/`DeviceAuthenticationFailed` + (6A80 or a failed card proof) to the existing incorrect-code prompt; + duplicate-name 6985 and storage-full statuses keep their UI mapping; other + protocol and transport failures propagate without retry or replay. +- OATH set-default merges the former `setDefault`/`setDefaultLegacy` into one + `oathSetDefault` call; the legacy dialect is dispatched by the upstream + `Capability::OathSetDefaultSlots`. On legacy firmware, long slots and + appendEnter are rejected as `InvalidArgument` at construction with zero I/O + and the UI shows notSupported; a 6984 response maps NotFound to the + operationFailed prompt. `OathCardClient.transceive` and the dead + setDefaultLegacy path were removed; OATH no longer builds APDUs by hand. +- NDEF reads/writes use the profile-free `ndefReadCapability`/`ndefReadMessage`/ + `ndefWriteMessage` bindings; upstream owns SELECT, the 240-byte chunking and + the crash-safe three-phase write. A 6A82 on SELECT yields a null read / false + write capability, and 6982 maps to `NdefReadOnlyException`. All manual + chunking, NLEN and capability-container parsing was removed from + `ndef_card.dart`. +- CTAP/WebAuthn transport is a thin layer over `ctapSelectApplication` plus + `ctapTransceiveSelected`; selection evidence is tracked per lease and a new + lease reselects automatically. All six raw SELECT FIDO2 AID sites in + webauthn_controller, the manual 80100000 envelope and the 80C00000 + continuation loop were removed. Non-zero CTAP statuses pass through to the + fido2 package as data; CBOR/ClientPin framing stays in Dart. +- Pass reads/writes use `adminPassSlots` (read, not PIN-protected) and + `adminSetPassSlot` (protected) with the same thin client shape as + admin_card; both reuse the lease's verified Admin session when one is + recorded and otherwise SELECT + verify an explicit per-request PIN. Unknown + slot types display as unknown, and a write reporting `reprobeRequired` + invalidates the profile. The manual 0043/0044 commands were removed. +- New facade bindings in `rust/src/api/protocol.rs`: `adminPassSlots`/ + `adminSetPassSlot` (including the `AdminValueKind.passSlots` encoding), + `Access::Existing` support (the `existing` parameter) on the whole Admin + request path (`adminRead`/`adminConfigure`/`adminAction` and the PASS slot + operations), `pivImportPqSeed`, `oathSetDefault`, `ndefReadCapability`/ + `ndefReadMessage`/`ndefWriteMessage` and `ctapSelectApplication`/ + `ctapTransceiveSelected`/`ctapTransceive`. + Existing UI models still map validated configuration bytes to display/algorithm -fields. Other Admin/PIV/OATH/OpenPGP/NDEF/Pass operations remain in Dart. +fields. The WebAuthn CBOR/ClientPin layer and the keyboard keymap commands remain +in Dart. Admin coverage and the specific upstream gaps are described below. + +## Admin integration + +Admin uses an explicit minimal probe, which observes firmware/model/serial and does +not select PIV. Its profile belongs to the physical lease and is never fabricated +from UI firmware labels. Identity reads reuse those observations; other reads use +`admin::operation`, including version-dependent configuration parsing, chip ID, +core commit, NFC state, physical/logical storage and SM2 configuration. Only an +explicit `UnsupportedFeature` makes optional core-commit/applet-usage reads absent; +security, malformed replies and unknown capabilities remain failures. + +The pinned Admin facade SELECTs and verifies a PIN only when explicitly +supplied; with `Access::Existing` it sends neither and reuses the caller's +selected, already authorized transaction. Console records the evidence for +that reuse only on a successful explicit facade `VerifyPin` (itself +`existing: false`): the lease keeps an authentication stamp bound to its +selection and profile generations. Any applet selection (`willSelectApplet`), +profile invalidation, exposed or uncertain Admin mutation, failed Existing +request, or lease rebind/replacement expires the stamp, and later protected +requests fall back to `existing: false` plus an explicit per-request PIN. +The card remains the enforcement point: a wrongly assumed session surfaces as +a security rejection, which itself drops the stamp. The UI PIN cache stays +prompting/cache policy and never creates evidence. The controller's +authentication step still validates the PIN for prompting/cache policy and +leaves Admin selected for remaining legacy consumers. The settings refresh +skips its redundant re-probe while the prepared binding is still valid +(`prepareIfStale`), so one page load performs a single probe and a single +SELECT+VERIFY, with chip ID, core commit, configuration, NFC, storage and +keyboard reads reusing the session. The Pass page's explicit probe after +authentication re-selects Admin and therefore ends the evidence, so its +protected slot write keeps the per-request PIN sequence. Mutable PIN inputs +are wiped; the short-lived service copy belongs to the verified physical +lease and is cleared on rebind, release or controller disposal. A repolled +device's observed serial is checked before submitting a cached PIN. +Duplicate local/persisted PIN candidates are not automatically retried. +PIN change drops cached old credentials before the attempt; a failed or lost +acknowledgment does not restore them. Credential-cache invalidation +generations also expire page-local Admin/OATH/WebAuthn copies after reset or +PIN removal. Factory reset invalidates both saved and unsaved credentials +for that serial before the attempt; unrelated devices and preferences remain +intact. + +Configuration patches, PIN change, NFC/NDEF settings, historical keyboard flags and +OpenPGP touch, SM2 writes, applet resets and factory reset now use upstream requests. +Feature changes carry only the selected mask; libcanokey reads the current device +configuration, preserves unselected fields and refuses to overwrite unknown feature +bits. Factory reset submits no PIN guesses; blocked-PIN and presence requirements +remain card-enforced. The existing request to reset is not an automatic retry. + +The executor copies Admin progress before closing its handle, including after a +transport exception or cancellation. `lastProgress` retains acknowledged-write count +and `reprobeRequired`. Once a profile-affecting write is exposed, success, card +failure, malformed acknowledgment, cancellation and lost transport response all +invalidate profile evidence. Controllers explicitly prepare before a subsequent +operation; the executor never re-probes, retries, resumes or rolls back a mutation. +Multiple settings operations are not an atomic transaction. No-op patches need no +profile invalidation. A cancelled write whose acknowledgment was not processed is +exposed but unconfirmed, rather than falsely reported successful. + +Each lease now tracks selection and profile-evidence generations as well as its +physical connection generation. Admin SELECT invalidates previously prepared PIV +selection; Admin configuration writes invalidate profiles held by other clients on +the same lease. Injected transports can share an explicit caller-owned `CardLease` +for the same cross-applet contract. Unknown raw commands are not classified by +parsing APDUs in Dart; remaining legacy callers still own their selection boundary. + +Modern SM2 IDs use upstream signed big-endian validation and read/patch behavior. +For audited 3.0.x CanoKey devices, Console explicitly interprets the legacy packed +native i32 fields as little-endian and retains that encoding when editing them. +libcanokey validates the legacy nine-byte object without claiming portable byte +order. The result variant comes from the probed firmware, not guessed reply length. + +### Upstream gaps and remaining boundaries + +- **Admin selected-context operations:** converged. Protected Admin requests + reuse `Access::Existing` while the lease holds valid recorded session + evidence (see Admin integration above) and otherwise keep `existing: false` + with an explicit per-request PIN. `Request::VerifyPin` never uses Existing + (upstream rejects it), so the explicit authentication step is unchanged. +- **WebAuthn CBOR/ClientPin:** the CBOR framing and ClientPin protocol (fido2 + package) stay in Dart as a host-side policy choice, not an upstream gap. + Only the CTAP transport envelope (80100000) and the 80C00000 continuation + loop moved upstream. +- **Keyboard keymap read/write/reset (45/46/47):** migrated; these commands + are routed through upstream Admin operations (see the existing-access + increment below). No Dart-side raw keymap exchanges remain. +- Connection-bootstrap identity APDUs in SmartCard now use the upstream + `admin::command::select`/`admin::command::serial` builders through the + facade's bootstrap binding, on the same exclusive queue as before. The + PIV macOS-setup serial read is the only remaining raw exchange in Dart. +- Firmware outside the pinned library's audited capability matrix remains + `CapabilityUnknown`/unsupported. Console does not bypass capability checks with + invented profiles or speculative wire formats. + +## Connection and operation lifecycle + +This increment follows upstream [Console integration](https://github.com/canokeys/libcanokey/blob/95e1930ea86eb248d0295bce9880cee5c65fd85e/docs/console-integration.md) +and [API contracts](https://github.com/canokeys/libcanokey/blob/95e1930ea86eb248d0295bce9880cee5c65fd85e/docs/api-design.md): + +1. `SmartCard.process` queues the entire use case, including connection setup, + SELECT, authentication, dependent commands, result publication and cleanup. + CCID connection/probe APDUs use the same queue; reader enumeration can observe + disconnection immediately and invalidate the active generation. +2. A lease captures the physical CCID handle or the polled NFC/WebUSB channel. + New polls explicitly bind a new generation. A closed, failed or replaced lease + cannot exchange or publish an executor result. A default SmartCard protocol + executor requires a `process` scope and captures one lease for its whole loop; + it never silently reacquires a connection between APDUs. +3. Dart owns the opaque Rust operation across each await. Generation/cancellation + checks occur before start/exchange, after I/O and before returning owned bytes. + Only the executor advances/closes/disposes its handle. PIV controller disposal + requests cancellation of migrated operations, without freeing their handles. +4. Cancellation waits for outstanding I/O to settle before releasing the use-case + lock. A transport exception poisons the lease and triggers awaited backend + disconnect/finish, without command replay. Failed cleanup quarantines the + channel until application restart. No Dart `Future.timeout` unlocks early. +5. PIN and forced-PIN-change prompt callbacks retain their owner zone across UI + events. Double submits cannot overlap exchanges. Explicit re-poll replaces the + old lease; closed dialog callbacks cannot revive a completed session. + +Operation close only releases local memory. It does not log out, reconnect, undo +writes or roll back card state. Injected transports in transcript tests still own +and isolate their fake connection; they can pass an explicit lease to the executor. +Standalone legacy raw exchanges respect the queue but do not establish a multi-APDU +transaction: production use cases must keep using `SmartCard.process`. This is +application-local exclusivity; it does not add an OS-level PC/SC transaction API +to the CCID plugin. + +`DeviceProfile` now lives only within a prepared physical lease; no profile or +access state is cached across use cases. PIV controller use cases explicitly prepare before dependent reads and any +authentication. An explicit SELECT invalidates the previous preparation. Raw +controller commands and selected reads also respect active operation reservations, +including with injected transports. +Injected transports use `PivCardClient.withSession` for this same ownership contract; +the USB/IP metadata smoke path has been updated accordingly. -## Session boundary and next increments +The profile is immutable evidence, not an authentication token. Metadata factories +use `Access::Existing` without asserting PIN or management authorization. Operations +copy their required profile evidence and survive source-profile close; Dart still +checks the original lease before every exchange and publication. Cancelled or stale +probe results are closed/disposed instead of being published. Lease cleanup releases +all registered resources even when one destructor fails, and always releases the +use-case queue. -The caller must hold its selected card session for the entire executor future. -The executor does not acquire a connection or establish a transaction. Current -callers continue to use Console's existing SmartCard workflows; this increment -does not add a global connection lock or profile cache. +Credential and management authentication factories explicitly omit SELECT without +asserting prior authorization. There is no host authentication cache. Uncertain +credential/authentication outcomes invalidate preparation; callers must not continue +dependent work or automatically re-probe in that transaction. Controllers await +their use cases so protocol failures propagate and cancelled polling completes +without leaving pending result futures. -Before migrating authenticated operations, establish a connection-bound exclusive -lease with generation checks, and derive a profile from observations of that same -device. Keep the lease across SELECT, authentication, dependent operations and -publication of results. Never SELECT/probe between authentication and its target. -Use upstream `*_in_context` factories for the already selected session; do not -assert authentication from cached UI state. Drain failed/timed-out I/O before -reusing the connection. Operation close is local cleanup, not card rollback. +Key operations, certificate writes and management-key replacement in the controller +now use the upstream `Access::Existing` factories within the same reserved +transaction: signing (`piv_sign` with host-side digest/padding), X25519 derive, +ML-KEM-768 decapsulation, EC/RSA/Ed25519 key import, management-key replacement +and algorithm-configuration writes. A successful algorithm-configuration write +discards the prepared profile (upstream marks it `ReprobeRequired`). Certificate +imports go through `write_object`; libcanokey owns the 53 framing and PUT DATA +segmentation. The macOS setup serial reuses the probed Admin observation instead +of a raw Admin SELECT. Never infer access from UI PIN caches, or SELECT/probe between +authentication and its target. Host CSR construction and UI policy remain in Console. -Suggested next steps are profile/session ownership, certificate and metadata -reads, PIN/PUK and management authentication, PIV writes/private operations, then -Admin/OATH/OpenPGP. Host CSR construction and UI policy remain in Console. +Streaming signing and attestation complete the facade: `piv_sign_streaming` +(mode 0 = ML-DSA-65 with the empty context, 1 = randomized Ed25519 wire mode FF, +2 = SM2 full message with an optional user ID) and `piv_attest` (INS F9) both use +`Access::Existing` inside the reserved transaction. ML-DSA-65 messages and Ed25519 +messages that are empty or exceed the classic 512-byte budget sign through the +streaming modes; classic digest signing (including the host-computed SM2 ZA +digest) stays with `piv_sign`. Streaming modes require evidenced 3.1.0 firmware +with observed enabled algorithm IDs, so pre-3.1 devices lose the 513–544-byte and +empty-message classic Ed25519 paths Console previously attempted. The controller +no longer builds GENERAL AUTHENTICATE or attestation APDUs. Post-quantum seed +import now uses the `pivImportPqSeed` binding with `Access::Existing` and a +prepared profile: the P1 algorithm ID comes from the probed profile, so an +unprobed PQ ID fails `CapabilityUnknown` before I/O, and the Default policy no +longer sends explicit AA/AB TLVs. `piv_post_quantum.dart`, `_sendChainedData`, +`_algorithmIdHex` and `PivCardClient.transceive` were removed; PIV contains no +hand-built APDUs. ## Verification @@ -52,7 +321,7 @@ Admin/OATH/OpenPGP. Host CSR construction and UI policy remain in Console. flutter_rust_bridge_codegen generate cargo test --manifest-path rust/Cargo.toml api::protocol --locked cargo build --manifest-path rust/Cargo.toml --release --locked -flutter test --no-pub test/helper/utils/piv_card_test.dart +flutter test --no-pub test/helper/utils/piv_card_test.dart test/helper/utils/fido2_backend_test.dart flutter test --no-pub --exclude-tags native ``` @@ -60,7 +329,7 @@ The native PIV transcript tests run alongside FIDO2 tests in the USB/IP workflow The firmware smoke entry point initializes FRB before the migrated PIV operations. Native transcripts do not establish physical-device or legacy-firmware coverage. -Local validation on 2026-09-15 (Rust 1.98.1 / Flutter 3.47.1): all 35 Rust tests, +First-increment validation on 2026-09-15 (Rust 1.98.1 / Flutter 3.47.1): all 35 Rust tests, 292 non-native Dart/UI tests and 19 native PIV/FIDO2 tests passed. The generated bridge built natively, `cargo check --target wasm32-unknown-unknown` and `flutter build web --no-pub` passed. Changed Dart files passed analysis and the @@ -69,3 +338,150 @@ workflow passed actionlint. Strict Clippy encountered the existing passed with only that lint allowed. Repository-wide rustfmt also reports existing formatting in `piv_crypto.rs`; the new protocol module passes rustfmt. No physical card, browser transport session or USB/IP firmware matrix was run locally. + +Certificate/session increment validation on 2026-09-15: 37 Rust tests, 303 +non-native Dart/UI tests and 21 native PIV/FIDO2 tests passed. Coverage includes +exclusive use cases, expired callback leases, late responses after disconnect, +cancellation during I/O, poisoned transports, invalid/duplicate certificate TLVs, +gzip CRC/trailing-data failures and decompression limits. Changed Dart files passed +analysis, native release build and wasm32 check passed, and the FRB WASM package +and Flutter web application both built. The web helper initially hit the known +host-linker-flags failure; rebuilding with a temporary wasm-bindgen-cli 0.2.100 +installation resolved it without replacing the global tool. No physical-card, +browser transport or USB/IP firmware matrix was exercised for this increment. + +Profile/metadata increment validation on 2026-09-15: all 39 Rust tests and 306 +non-native Dart/UI tests passed. The final focused run passed 56 tests, including +28 native PIV/FIDO2 tests and session, executor, metadata model and controller +coverage. It verifies profile ownership and disposal, operation-wide exclusion, +failed re-probes, metadata after authentication without SELECT, legacy firmware +responses, malformed metadata and algorithm resolution against the current device +profile rather than cached UI configuration. Changed handwritten Dart files passed +analysis; the protocol module passed rustfmt and the diff passed whitespace checks. +The native release, wasm32 check, regenerated FRB WASM package and final Flutter web +application build passed. No physical-card, browser transport or USB/IP firmware +matrix was exercised for this increment. + +Credential/authentication increment validation on 2026-09-15: all 42 Rust tests, +300 non-native Dart/UI tests and 53 focused session/executor/native tests passed +(34 native PIV/FIDO2 tests). Six obsolete Dart authentication-builder tests were +removed with the unused implementation; native transcripts now exercise the +upstream authentication path, including TDES/AES known-answer vectors, legacy Le, +concurrent-command exclusion, malformed challenges, credential references/retries, +terminal mutation failures and profile invalidation. After updating controller +completion handling, its 81 controller/view tests also passed. Changed handwritten +Dart files passed analysis, the protocol module passed rustfmt, and the diff passed +whitespace checks. Native release, the regenerated FRB WASM package and Flutter web +application built successfully. No physical-card, browser transport or USB/IP +firmware matrix was exercised for this increment. + +Admin increment validation on 2026-09-15: all 44 Rust tests, 290 non-native Dart/UI +tests and 76 focused native/session/executor tests passed (57 native tests across +Admin, applet switches, PIV and FIDO2). The final selected-app publication checks +passed another 48 Admin/PIV tests. Coverage includes explicit per-request PINs, +legacy configuration and SM2 layouts, masked/no-op patches, retained partial-write +progress, cancellation/lost responses, no mutation replay, cross-applet selection +and profile invalidation, and cross-page credential-cache invalidation. Changed +handwritten Dart files passed analysis, the protocol module passed rustfmt and the +diff passed whitespace checks. Native release, regenerated FRB WASM and Flutter web +builds passed. The native workflow includes the new Admin/applet-switch tests; its +YAML parses successfully. No physical-card, browser transport or USB/IP firmware +matrix was exercised locally. + +## Existing-access increment and ownership boundary + +All libcanokey Cargo crates now pin `95e1930e` (including protocol and admin). The removed upstream selected-context +allocation has been replaced by `Access::Existing` or explicit `select=false`. +No profile is an authentication token: Console retains the physical lease, +selection/profile generation checks, operation ownership and cancellation cleanup. + +Console's executor records whether an exchange was attempted separately from the +upstream protocol error. This local fact governs whether failed prepared operations +may retain their lease evidence; protocol phase is not an I/O receipt. Expected +credential rejections and absent object reads retain their documented handling. + +Confirmed upstream issue: object/certificate reads at this revision can report +`InvalidResponse/Construction` for malformed outer containers returned by the +card. The missing `Parsing` annotation belongs to canokey-piv. A separate local +The pinned revision includes the PIV object parsing phase fix; Console performs +no protocol error remapping. Profile-free selected PIN status remains an upstream API gap in this revision; Admin existing-selection requests are available at the current pin and now carry Console's lease-evidenced Admin session reuse (see Admin integration). Keyboard layout/keymap reads and writes are now routed through upstream Admin operations. + +Validation for this increment: 44 Console Rust tests, 290 non-native Dart/UI tests, +82 focused native/session/executor tests and a final 46-test PIV/executor run passed. +Native release, regenerated FRB WASM and final Flutter Web builds passed; changed +handwritten Dart files passed analysis. The upstream regression failed before the +fix, then all 67 canokey-piv tests (including doctests) and strict workspace/all-target +clippy passed. Neither repository was tested with physical hardware or the USB/IP +firmware matrix in this increment. The libcanokey fix is committed locally only. + +OpenPGP increment validation on 2026-09-16: the 12 focused OpenPGP card tests +(native transcripts over injected transports) and the 12 OpenPGP page tests +passed, and changed Dart files passed analysis. The wider non-native suite +passed 260 tests with 5 pre-existing OATH compile-failure suites untouched by +this increment (`oath_card.dart` belonged to the parallel OATH migration). The +host needed an `xcrun` shim exporting `DEVELOPER_DIR` for the Command Line +Tools SDK because the Xcode license prompt breaks the objective_c native-asset +hook. + +OpenPGP administrative-writes increment validation on 2026-09-16: the bridge +gained `openpgp_write_reset_code` (Option clears), `openpgp_reset_retries`, +`openpgp_write_signature_pin_policy`, `openpgp_unblock_with_admin`, +`openpgp_unblock_with_code`, `openpgp_write_touch_policy` and +`openpgp_write_touch_cache_time`; all seven Console operations moved off raw +APDUs, and the client's manual SELECT/PUT DATA/RESET RETRY COUNTER builders +were removed. All 53 Rust tests passed, including new wire-byte, argument +validation, capability-gate (UIF from 1.5.2, retry reset from 3.1.0) and +credential-reference/reset-code error-mapping coverage. After FRB +regeneration, the 14 native OpenPGP card tests plus focused PIV/Admin native +suites and the 12 OpenPGP page tests passed (81 tests), changed handwritten +Dart files passed analysis, and `cargo build --release --locked` passed. +Reset-code clearing is a dataless PUT DATA D3 (no Lc) on the wire, matching +upstream framing. The USB/IP smoke harness keeps its session/prepare flow +but was not run locally; no physical card, browser transport or USB/IP +firmware matrix was exercised. + +OATH increment validation on 2026-09-16: all 53 Rust tests passed, including +new protocol-module coverage for per-operation access validation, protected +applet rejection before the target command, the name/marker-preserving +calculation encoding and the legacy 1.3 dialect gate. The 9 native OATH card +tests (injected-transport transcripts with canokey-oath known-answer HMACs), +the OATH model/view tests and the full non-native suite (276 tests) passed, as +did the focused native PIV/Admin/session/executor suites (74 tests) after the +FRB regeneration. Changed Dart files passed analysis; `cargo build --release +--locked` passed. FRB WASM regeneration and the Flutter web build were not run +locally, and no physical card, browser transport or USB/IP firmware matrix was +exercised. + +PQ-import/OATH-set-default/NDEF/CTAP/Pass increment validation on 2026-09-16: +the pin moved to `95e1930e`, which brings the new canokey-ctap and canokey-ndef +crates plus upstream PIV PQ seed import, OATH SetDefault, Admin +PassSlots/SetPassSlot and `admin::Access::Existing`/`operation_with_access`. +`cargo test --locked` passed in full (65 tests, including 9 new facade-binding +tests). New native-transcript Dart tests: 4 PIV PQ, 6 OATH set-default, 10 +NDEF, 11 CTAP and 8 Pass. The full `flutter test` suite passed (376 tests). +The FRB WASM package was regenerated and `flutter build web --no-pub` passed +(host-side wasm-bindgen-cli 0.2.100 pre-installed to dodge the known +host-linker-flags leak from the wasm RUSTFLAGS). The USB/IP firmware +matrix and physical-card runs were not exercised for this increment. + +Admin existing-access increment validation on 2026-09-16: the `existing` +parameter now reaches `adminRead`/`adminConfigure`/`adminAction`, and Console +records lease-bound Admin session evidence only on an explicit facade +`VerifyPin`, stamped with the lease's selection/profile generations. +Protected requests reuse `Access::Existing` while that evidence is valid and +fall back to `existing: false` plus an explicit per-request PIN after any +applet selection, profile invalidation, exposed/uncertain mutation, failed +Existing request or lease replacement. `cargo test --locked` passed in full +(66 tests, including existing-access wire coverage for the three general +Admin entries). 124 native-tagged Dart tests passed, including new +transcripts asserting that Existing requests send no SELECT/VERIFY, that a +confirmed mutation, a cross-applet selection (PIV probe) and a failed +Existing request each end the evidence, and that the Pass client's own probe +on a shared lease ends another client's recorded session. The non-native +suite passed (257 tests, including the lease-evidence generation unit tests +and the settings configuration tests against the `prepareIfStale` refresh). +Changed Dart files passed analysis and the protocol module passes rustfmt. +After this increment the FRB WASM package was regenerated again and +`flutter build web --no-pub` passed, strict clippy is warning-free and the +full `flutter test` run passed (381 tests). No physical card, browser +transport or USB/IP firmware matrix was exercised for this increment. diff --git a/lib/controller/applets/oath/oath_controller.dart b/lib/controller/applets/oath/oath_controller.dart index 8c999eb..41ad77e 100644 --- a/lib/controller/applets/oath/oath_controller.dart +++ b/lib/controller/applets/oath/oath_controller.dart @@ -1,20 +1,17 @@ import 'dart:async'; -import 'dart:convert'; import 'package:canokey_console/controller/applets/oath/qr_scan_result.dart'; import 'package:canokey_console/controller/base/polling_controller.dart'; import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; import 'package:canokey_console/helper/theme/admin_theme.dart'; -import 'package:canokey_console/helper/tlv.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/oath_card.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/helper/widgets/input_pin_dialog.dart'; import 'package:canokey_console/models/oath.dart'; -import 'package:canokey_console/src/rust/api/crypto.dart'; -import 'package:convert/convert.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -26,7 +23,7 @@ class OathController extends PollingController { final OathCardClient _client = OathCardClient(); final TimerController timerController = TimerController.seconds(30); final Rxn qrScanResult = Rxn(); - final Map _localCodeCache = {}; + final _localCodeCache = CredentialCache('OATH'); final Map oathMap = {}; OathVersion version = OathVersion.v1; @@ -55,6 +52,7 @@ class OathController extends PollingController { @override void onClose() { + _client.cancelPendingOperations(); timerController.dispose(); super.onClose(); } @@ -63,11 +61,12 @@ class OathController extends PollingController { Future doRefreshData() async { log.t('Call OathController.doRefreshData'); await SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } - await _refresh(); + await _refresh(key); }); } @@ -75,30 +74,36 @@ class OathController extends PollingController { OathAlgorithm algo, int digits, bool requireTouch, int initValue) async { log.t('Call OathController.addAccount'); await SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } - final resp = await _client.put( - name: name, - secretHex: secretHex, - type: type, - algorithm: algo, - digits: digits, - requireTouch: requireTouch, - initialValue: initValue, - ); - if (resp == '6985') { - Prompts.showPrompt( - S.of(Get.context!).oathDuplicated, ContentThemeColor.danger); - return; - } - if (Prompts.isStorageFull(resp)) { - Prompts.showPrompt( - S.of(Get.context!).storageFull, ContentThemeColor.danger); - return; + try { + await _client.put( + name: name, + secretHex: secretHex, + type: type, + algorithm: algo, + digits: digits, + requireTouch: requireTouch, + initialValue: initValue, + key: key, + ); + } on ProtocolException catch (e) { + if (e.details.statusWord == 0x6985) { + Prompts.showPrompt( + S.of(Get.context!).oathDuplicated, ContentThemeColor.danger); + return; + } + final sw = _client.lastStatusWord; + if (sw != null && Prompts.isStorageFull(sw)) { + Prompts.showPrompt( + S.of(Get.context!).storageFull, ContentThemeColor.danger); + return; + } + rethrow; } - SmartCard.assertOK(resp); log.i('Successfully added $name'); Navigator.pop(Get.context!); @@ -106,51 +111,55 @@ class OathController extends PollingController { S.of(Get.context!).oathAdded, ContentThemeColor.success, forceSnackBar: true); - await _refresh(); + await _refresh(key); }); } Future setCode(String newCode, bool saveCode) async { log.t('Call OathController.setCode'); await SmartCard.process((String sn) async { - String resp = await _transceive('00A4040007A0000005272101'); - SmartCard.assertOK(resp); - if (resp == '9000') { + await _client.prepare(); + final selection = await _client.select(); + version = selection.version; + if (version == OathVersion.legacy) { log.w('Code not supported'); return; - } else { - if (!await _authenticate(sn)) { + } + final salt = selection.salt!; + Uint8List? oldKey; + if (selection.requiresCode) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } + oldKey = key; + } - Map info = TLV.parse(hex.decode(SmartCard.dropSW(resp))); + try { if (newCode.isEmpty) { - // clear code - resp = await _transceive('00030000027300'); + await _client.clearCode(key: oldKey); } else { - final key = pbkdf2HmacSha1( - password: newCode, - salt: info[0x71], - iterations: 1000, - keyLen: 16); - final mac = hmacSha1(key: key, data: List.of([0, 0, 0, 0])); - resp = await _transceive( - '000300002F731101${hex.encode(key)}7404000000007514${hex.encode(mac)}'); - } - - SmartCard.assertOK(resp); - log.i('Successfully changed code'); - - _localCodeCache[sn] = newCode; - if (saveCode) { - await LocalStorage.setPinCache(sn, _tag, newCode); + final newKey = OathCardClient.deriveKey(newCode, salt); + try { + await _client.setCode(newKey: newKey, oldKey: oldKey); + } finally { + newKey.fillRange(0, newKey.length, 0); + } } + } finally { + oldKey?.fillRange(0, oldKey.length, 0); + } + log.i('Successfully changed code'); - Navigator.pop(Get.context!); - Prompts.showPrompt( - S.of(Get.context!).oathCodeChanged, ContentThemeColor.success, - forceSnackBar: true); + _localCodeCache[sn] = newCode; + if (saveCode) { + await LocalStorage.setPinCache(sn, _tag, newCode); } + + Navigator.pop(Get.context!); + Prompts.showPrompt( + S.of(Get.context!).oathCodeChanged, ContentThemeColor.success, + forceSnackBar: true); }); } @@ -158,7 +167,8 @@ class OathController extends PollingController { log.t('Call OathController.calculate'); late String code; await SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } @@ -167,15 +177,14 @@ class OathController extends PollingController { int challenge = DateTime.now().millisecondsSinceEpoch ~/ 30000; challengeHex = challenge.toRadixString(16).padLeft(16, '0'); } - final resp = await _client.calculate( + final (digits, rawCode) = await _client.calculate( name: name, type: type, challengeHex: challengeHex, + key: key, ); - SmartCard.assertOK(resp); - - List data = hex.decode(SmartCard.dropSW(resp)); - code = _parseResponse(data.sublist(2), oathMap[name]!.format); + code = formatOathCode( + rawCode: rawCode, digits: digits, format: oathMap[name]!.format); oathMap[name]!.code = code; _startTimer(); @@ -187,32 +196,53 @@ class OathController extends PollingController { Future delete(String name) async { log.t('Call OathController.delete'); await SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } - SmartCard.assertOK(await _client.delete(name)); + await _client.delete(name, key: key); log.i('Successfully deleted $name'); Navigator.pop(Get.context!); Prompts.showPrompt(S.of(Get.context!).deleted, ContentThemeColor.success, forceSnackBar: true); - await _refresh(); + await _refresh(key); }); } Future setDefault(String name, int slot, bool withEnter) async { log.t('Call OathController.setDefault'); await SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { + final (authenticated, key) = await _authenticate(sn); + if (!authenticated) { return; } - List nameBytes = utf8.encode(name); - String capduData = - '71${nameBytes.length.toRadixString(16).padLeft(2, '0')}${hex.encode(nameBytes)}'; - SmartCard.assertOK(await _transceive( - '00550$slot${withEnter ? '01' : '00'}${(capduData.length ~/ 2).toRadixString(16).padLeft(2, '0')}$capduData')); + try { + // The dialog numbers slots from 1; the protocol numbers from 0. + await _client.setDefault( + name: name, + slot: slot - 1, + appendEnter: withEnter, + key: key, + ); + } on ProtocolException catch (e) { + // Legacy single-slot firmware rejects a long slot or an appended + // Enter before any I/O; the nameless 6984 reply maps to NotFound. + if (e.details.kind == 'InvalidArgument' && + e.details.phase == 'Construction') { + Prompts.showPrompt( + S.of(Get.context!).notSupported, ContentThemeColor.warning); + return; + } + if (e.details.kind == 'NotFound') { + Prompts.showPrompt( + S.of(Get.context!).operationFailed, ContentThemeColor.danger); + return; + } + rethrow; + } log.i('Successfully changed default'); Navigator.pop(Get.context!); @@ -222,23 +252,6 @@ class OathController extends PollingController { }); } - void setDefaultLegacy(String name) { - log.t('Call OathController.setDefaultLegacy'); - SmartCard.process((String sn) async { - if (!await _authenticate(sn)) { - return; - } - - List nameBytes = utf8.encode(name); - String capduData = - '71${nameBytes.length.toRadixString(16).padLeft(2, '0')}${hex.encode(nameBytes)}'; - SmartCard.assertOK(await _transceive( - '00550000${(capduData.length ~/ 2).toRadixString(16).padLeft(2, '0')}$capduData')); - Prompts.showPrompt( - S.of(Get.context!).successfullyChanged, ContentThemeColor.success); - }); - } - void parseUri(String keyUri) { log.t('Call OathController.parseUri'); final uri = Uri.parse(keyUri); @@ -284,51 +297,46 @@ class OathController extends PollingController { initValue: counter); } - Future _verifyCode(String code) async { - final selection = await _client.select(); - version = selection.version; - String resp = selection.response; - final info = selection.info; - if (!info.containsKey(0x74)) { - // no code set - return true; - } - List nonce = info[0x71]; - List challenge = info[0x74]; - final key = pbkdf2HmacSha1( - password: code, salt: nonce, iterations: 1000, keyLen: 16); - final mac = hmacSha1(key: key, data: challenge); - resp = await _transceive('00A300001C7514${hex.encode(mac)}740400000000'); - if (resp == '6a80') { - Prompts.showPrompt( - S.of(Get.context!).pinIncorrect, ContentThemeColor.danger); - return false; + /// Validate a candidate code against the card. An incorrect code reports + /// and returns null; protocol and transport failures propagate. + Future _verifyCode(String code, List salt) async { + final key = OathCardClient.deriveKey(code, salt); + try { + await _client.validate(key); + return key; + } on ProtocolException catch (e) { + key.fillRange(0, key.length, 0); + if (e.details.kind == 'AuthenticationFailed' || + e.details.kind == 'DeviceAuthenticationFailed') { + Prompts.showPrompt( + S.of(Get.context!).pinIncorrect, ContentThemeColor.danger); + return null; + } + rethrow; } - SmartCard.assertOK(resp); - return true; } - /// Returns true if CanoKey is authenticated. + /// Prepares and selects the applet, then returns the validated access key + /// for this use case, or null when the applet requires no code. + /// `(false, null)` means the user cancelled or no candidate matched. /// /// We first try to use the local cache. If not cached, try LocalStorage. /// Finally, prompt the user for code. - Future _authenticate(String sn) async { + Future<(bool, Uint8List?)> _authenticate(String sn) async { // First check if authentication is required + await _client.prepare(); final selection = await _client.select(); version = selection.version; - if (version == OathVersion.legacy) { - return true; - } - final info = selection.info; - if (!info.containsKey(0x74)) { - // no code set - return true; + if (version == OathVersion.legacy || !selection.requiresCode) { + return (true, null); } + final salt = selection.salt!; // Try local cache first if (_localCodeCache.containsKey(sn)) { - if (await _verifyCode(_localCodeCache[sn]!)) { - return true; + final key = await _verifyCode(_localCodeCache[sn]!, salt); + if (key != null) { + return (true, key); } _localCodeCache.remove(sn); } @@ -336,9 +344,10 @@ class OathController extends PollingController { // Try LocalStorage String? codeToTry = LocalStorage.getPinCache(sn, _tag); if (codeToTry != null) { - if (await _verifyCode(codeToTry)) { + final key = await _verifyCode(codeToTry, salt); + if (key != null) { _localCodeCache[sn] = codeToTry; - return true; + return (true, key); } else { await LocalStorage.setPinCache(sn, _tag, null); } @@ -347,7 +356,7 @@ class OathController extends PollingController { // Finally, prompt user // When using NFC, we need to finish NFC before showing the dialog await SmartCard.stopPollingNfc(withInput: true); - final completer = Completer(); + final completer = Completer<(bool, Uint8List?)>(); InputPinDialog.show( title: S.of(Get.context!).oathInputCode, label: S.of(Get.context!).oathCode, @@ -364,9 +373,11 @@ class OathController extends PollingController { return; } Prompts.stopPromptAndroidPolling(); - bool verified = false; + Uint8List? key; try { - verified = await _verifyCode(code); + // The poll above bound a new lease; rediscover before validating. + await _client.prepare(); + key = await _verifyCode(code, salt); } on PlatformException catch (e) { await SmartCard.stopPollingNfc(withInput: true); log.e('_verifyCode failed', error: e); @@ -375,13 +386,13 @@ class OathController extends PollingController { S.of(Get.context!).interrupted, ContentThemeColor.danger); } } - if (verified) { + if (key != null) { log.t('PIN verified'); _localCodeCache[sn] = code; if (saveCode) { await LocalStorage.setPinCache(sn, _tag, code); } - completer.complete(true); + completer.complete((true, key)); // Since PIN has been cached, if error happens, we don't need to re-prompt SmartCard.nfcState = NfcState.processWithoutInput; // Close the dialog @@ -390,34 +401,14 @@ class OathController extends PollingController { }, onCancel: () async { SmartCard.nfcState = NfcState.idle; - completer.complete(false); + completer.complete((false, null)); }, ); return await completer.future; } - List _parse(List data) { - List result = []; - int pos = 0; - while (pos < data.length) { - OathItem item = _parseSingle(data.sublist(pos)); - pos += item.length; - result.add(item); - } - return result; - } - - OathItem _parseSingle(List data) { - assert(data.length >= 4); - assert(data[0] == 0x71); - - int nameLen = data[1]; - assert(4 + nameLen <= data.length); - String name = utf8.decode(data.sublist(2, 2 + nameLen)); - - int dataLen = data[3 + nameLen]; - assert(4 + nameLen + dataLen <= data.length); - + OathItem _itemOf(OathCalculatedEntry entry) { + final name = entry.name!; String issuer, account; int colon = name.indexOf(':'); if (colon == -1) { @@ -429,45 +420,27 @@ class OathController extends PollingController { } OathItem item = OathItem(issuer, account); - item.length = nameLen + dataLen + 4; - switch (data[2 + nameLen]) { - case 0x76: // response - item.code = _parseResponse( - data.sublist(4 + nameLen, 4 + nameLen + dataLen), item.format); - break; - case 0x77: // hotp - item.type = OathType.hotp; - break; - case 0x7C: // touch - item.requireTouch = true; - break; - default: - throw Exception('Illegal tag'); + if (entry.isHotp) { + item.type = OathType.hotp; + } + if (entry.requiresTouch) { + item.requireTouch = true; + } + final rawCode = entry.rawCode; + if (rawCode != null) { + item.code = formatOathCode( + rawCode: rawCode, digits: entry.digits, format: item.format); } return item; } - String _parseResponse(List resp, OathCodeFormat format) { - assert(resp.length == 5); - int digits = resp[0]; - int rawCode = (resp[1] << 24) | (resp[2] << 16) | (resp[3] << 8) | resp[4]; - return formatOathCode(rawCode: rawCode, digits: digits, format: format); - } - - Future _transceive(String capdu) async { - _client.version = version; - return _client.transceive(capdu); - } - - Future _refresh() async { + Future _refresh(Uint8List? key) async { int challenge = DateTime.now().millisecondsSinceEpoch ~/ 30000; String challengeStr = challenge.toRadixString(16).padLeft(16, '0'); - final resp = await _client.calculateAll(challengeStr); - SmartCard.assertOK(resp); - List data = hex.decode(SmartCard.dropSW(resp)); + final entries = await _client.calculateAll(challengeStr, key: key); polled = true; - var items = _parse(data); + var items = entries.map(_itemOf).toList(); // update oathMap with items for (var item in items) { if (oathMap.containsKey(item.name)) { diff --git a/lib/controller/applets/openpgp/openpgp_controller.dart b/lib/controller/applets/openpgp/openpgp_controller.dart index 5643950..882eb5f 100644 --- a/lib/controller/applets/openpgp/openpgp_controller.dart +++ b/lib/controller/applets/openpgp/openpgp_controller.dart @@ -21,6 +21,12 @@ class OpenPgpController extends PollingController { @override Logger get log => Logging.logger('OpenPGP:Controller'); + @override + void onClose() { + _client.cancelPendingOperations(); + super.onClose(); + } + @override Future doRefreshData() async { log.t('Call OpenPgpController.doRefreshData'); @@ -38,6 +44,7 @@ class OpenPgpController extends PollingController { } disabledMessage = null; + await _client.prepare(); cardInfo = await _client.readCardInfo(); polled = true; update(); @@ -128,6 +135,7 @@ class OpenPgpController extends PollingController { String Function()? successMessage, }) async { await SmartCard.process((String sn) async { + await _client.prepare(); final ok = await operation(); if (!ok) { Prompts.promptPinFailureResult(_client.lastStatusWord ?? ''); diff --git a/lib/controller/applets/pass/pass_controller.dart b/lib/controller/applets/pass/pass_controller.dart index 053053d..f46066d 100644 --- a/lib/controller/applets/pass/pass_controller.dart +++ b/lib/controller/applets/pass/pass_controller.dart @@ -6,6 +6,7 @@ import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/applet_switches.dart'; import 'package:canokey_console/helper/utils/pass_card.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/screenshot_mode.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/canokey.dart'; @@ -25,6 +26,12 @@ class PassController extends PollingController with AdminApplet { @override Logger get log => Logging.logger('Pass:Controller'); + @override + void onClose() { + _client.cancelPendingOperations(); + super.onClose(); + } + @override Future doRefreshData() async { log.t('Call PassController.doRefreshData'); @@ -61,6 +68,7 @@ class PassController extends PollingController with AdminApplet { return; } + await _client.prepare(); await _refresh(); }); } @@ -85,19 +93,35 @@ class PassController extends PollingController with AdminApplet { ); return; } - } else if (type == PassSlotType.oath) { + } else if (type == PassSlotType.oath || type == PassSlotType.unknown) { log.w('unsupported slot type'); return; } - final success = await _client.setSlot(index, type, password, withEnter); - if (!success && Prompts.isStorageFull(_client.lastStatusWord ?? '')) { - Prompts.showPrompt( - S.of(Get.context!).storageFull, - ContentThemeColor.danger, + final pin = adminPinForCurrentLease; + await _client.prepare(); + final bool success; + try { + success = await _client.setSlot( + index, + type, + password, + withEnter, + pin: pin, ); + } on ProtocolException { + if (Prompts.isStorageFull(_client.lastResponse ?? '')) { + Prompts.showPrompt( + S.of(Get.context!).storageFull, + ContentThemeColor.danger, + ); + return; + } + rethrow; + } + if (!success) { + Prompts.promptPinFailureResult(_client.lastResponse ?? ''); return; } - SmartCard.assertOK(_client.lastStatusWord ?? ''); log.i('Successfully changed slot'); Navigator.pop(Get.context!); @@ -107,6 +131,9 @@ class PassController extends PollingController with AdminApplet { forceSnackBar: true, ); + // A slot write invalidates profile evidence; explicitly re-probe before + // the dependent read instead of reusing stale evidence. + await _client.prepare(); await _refresh(); }); } diff --git a/lib/controller/applets/piv/piv_controller.dart b/lib/controller/applets/piv/piv_controller.dart index 037e717..dc7150a 100644 --- a/lib/controller/applets/piv/piv_controller.dart +++ b/lib/controller/applets/piv/piv_controller.dart @@ -1,7 +1,8 @@ import 'package:canokey_console/models/piv_macos_setup.dart'; import 'package:canokey_console/models/piv_self_sign_options.dart'; import 'dart:async'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' show PlatformInt64Util; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show PlatformInt64Util; import 'dart:math'; import 'dart:typed_data'; @@ -14,11 +15,10 @@ import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/piv_card.dart'; import 'package:canokey_console/helper/utils/piv_pin_retries.dart'; import 'package:canokey_console/helper/utils/piv_csr.dart'; -import 'package:canokey_console/helper/utils/piv_management_key.dart'; import 'package:canokey_console/helper/utils/piv_metadata_directory.dart'; -import 'package:canokey_console/helper/utils/piv_post_quantum.dart'; import 'package:canokey_console/helper/utils/piv_signature.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/piv.dart'; @@ -33,6 +33,13 @@ class PivController extends PollingController { PivController({PivCardClient? client}) : _client = client ?? PivCardClient(); final PivCardClient _client; + + @override + void onClose() { + _client.cancelPendingOperations(); + super.onClose(); + } + Map slots = {}; final Set certificateSlots = {}; final Map certificateBytes = {}; @@ -122,7 +129,7 @@ class PivController extends PollingController { disabledMessage = null; await _refreshCapabilities(); - await _client.select(); + await _client.prepare(); slots.clear(); certificateSlots.clear(); certificateBytes.clear(); @@ -133,10 +140,12 @@ class PivController extends PollingController { legacyPinRetriesRemaining = await _client.readRemainingPinRetries(); } pukInfo = supportsMetadata ? await _client.readMetadata(0x81) : null; - managementKeyInfo = - supportsMetadata ? await _client.readMetadata(0x9B) : null; - pinOnlyMode = - supportsPinOnlyMode ? await _readPinOnlyModeInSession() : false; + managementKeyInfo = supportsMetadata + ? await _client.readMetadata(0x9B) + : null; + pinOnlyMode = supportsPinOnlyMode + ? await _readPinOnlyModeInSession() + : false; final directory = await _readMetadataDirectory(); if (directory == null) { await _refreshSlotsLegacy(); @@ -167,12 +176,14 @@ class PivController extends PollingController { if (!supportsMetadataDirectory) { return null; } - final resp = await _client.transceive('00F7010000'); - if (!SmartCard.isOK(resp)) { - return null; - } try { - return PivMetadataDirectory.parse(hex.decode(SmartCard.dropSW(resp))); + return PivMetadataDirectory.parse(await _client.readMetadataDirectory()); + } on ProtocolException catch (error) { + if (error.details.kind == 'NotFound' || + error.details.kind == 'UnsupportedFeature') { + return null; + } + rethrow; } catch (error) { log.w('Failed to parse PIV metadata directory', error: error); return null; @@ -205,7 +216,7 @@ class PivController extends PollingController { SlotInfo? loaded = current; await SmartCard.process((String sn) async { - await _client.select(); + await _client.prepare(); if (needsKeyMetadata) { loaded = await _readKeyMetadata(slot) ?? current; } @@ -253,11 +264,18 @@ class PivController extends PollingController { Uint8List? bytes; try { bytes = await _client.readCertificate(certObject); - } on FormatException catch (error) { + } catch (error) { + if (error is! FormatException && + !(error is ProtocolException && error.details.phase == 'Parsing')) { + rethrow; + } // An unreadable object still occupies the slot: replacement needs consent. certificateSlots.add(slot); - log.w('Unable to decode PIV certificate object in slot ' - '${slot.toRadixString(16)}', error: error); + log.w( + 'Unable to decode PIV certificate object in slot ' + '${slot.toRadixString(16)}', + error: error, + ); return; } if (bytes == null) return; @@ -270,8 +288,11 @@ class PivController extends PollingController { slotInfo?.cert = cert; } on String catch (error) { // The Rust Result bridge throws decoding errors. - log.w('Unable to parse PIV certificate in slot ' - '${slot.toRadixString(16)}', error: error); + log.w( + 'Unable to parse PIV certificate in slot ' + '${slot.toRadixString(16)}', + error: error, + ); } } @@ -314,18 +335,25 @@ class PivController extends PollingController { _runPinChange(() => _client.unblockPin(puk, newPin), refresh: true); } - void _runPinChange(Future Function() operation, {bool refresh = false}) { - SmartCard.process((String sn) async { - await _client.select(); + void _runPinChange( + Future Function() operation, { + bool refresh = false, + }) async { + var changed = false; + await SmartCard.process((String sn) async { + await _client.prepare(); if (!await operation()) { Prompts.promptPinFailureResult(_client.lastStatusWord ?? ''); return; } Navigator.pop(Get.context!); - if (refresh) await refreshData(); + changed = true; Prompts.showPrompt( - S.of(Get.context!).successfullyChanged, ContentThemeColor.success); + S.of(Get.context!).successfullyChanged, + ContentThemeColor.success, + ); }); + if (changed && refresh) await refreshData(); } Future changeManagementKey( @@ -341,14 +369,17 @@ class PivController extends PollingController { if ((usePinOnly || storeOnDevice) && !supportsPinOnlyMode) { return false; } - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if ((usePinOnly || storeOnDevice) && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, currentKey, usePinOnly)) { + pin, + currentKey, + usePinOnly, + )) { c.complete(false); return; } @@ -387,106 +418,125 @@ class PivController extends PollingController { } c.complete(true); }); + if (!c.isCompleted) c.complete(false); return c.future; } Future generateCsr( - String slot, - AlgorithmType algorithm, - PinPolicy pinPolicy, - TouchPolicy touchPolicy, - String pin, - String managementKey, - Map subject, - List subjectAlternativeNames, - bool usePinOnly) async { + String slot, + AlgorithmType algorithm, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + String pin, + String managementKey, + Map subject, + List subjectAlternativeNames, + bool usePinOnly, + ) async { log.t('Call PivController.generateCsr'); final c = Completer(); - final data = _generateAsymmetricKeyData(algorithm, pinPolicy, touchPolicy); - final capdu = - '004700$slot${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}${data}00'; - - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (!await _verifyPinInSession(pin)) { c.complete(null); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(null); return; } - final resp = await _client.transceive(capdu); - if (!SmartCard.isOK(resp)) { + Uint8List spki; + try { + spki = await _client.generateKey( + slot: int.parse(slot, radix: 16), + algorithm: algorithm.value, + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + } catch (_) { c.complete(null); return; } - final publicKey = PivPublicKey.fromGenerateResponse( - algorithm, hex.decode(SmartCard.dropSW(resp))); + final publicKey = PivPublicKey.fromSubjectPublicKeyInfo(algorithm, spki); final certificationRequestInfo = PivCsrBuilder.buildCertificationRequestInfo( - subject: subject, - publicKey: publicKey, - subjectAlternativeNames: subjectAlternativeNames, - ); + subject: subject, + publicKey: publicKey, + subjectAlternativeNames: subjectAlternativeNames, + ); - final signResp = await _generalAuthenticate( + final signature = await _signInSession( slot, algorithm, certificationRequestInfo, publicKey, ); - if (!SmartCard.isOK(signResp)) { + if (signature == null) { c.complete(null); return; } - final signature = - _parseAuthenticateSignature(hex.decode(SmartCard.dropSW(signResp))); - c.complete(PivCsrBuilder.buildPem( - certificationRequestInfo: certificationRequestInfo, - algorithm: algorithm, - signature: signature, - )); + c.complete( + PivCsrBuilder.buildPem( + certificationRequestInfo: certificationRequestInfo, + algorithm: algorithm, + signature: signature, + ), + ); }); + if (!c.isCompleted) c.complete(null); return c.future; } - Future _readMacOsSetupSlotInSession(String slotNumber) async { + Future _readMacOsSetupSlotInSession( + String slotNumber, + ) async { final id = int.parse(slotNumber, radix: 16); final key = await _readKeyMetadata(id); if (key == null && _client.lastStatusWord?.toUpperCase() != '6A88') { throw StateError('Unable to inspect PIV key metadata'); } final cert = await _client.readCertificate(_certDO[id]!); - if (cert == null && !{'6A82', '6A88'}.contains(_client.lastStatusWord?.toUpperCase())) { + if (cert == null && + !{'6A82', '6A88'}.contains(_client.lastStatusWord?.toUpperCase())) { throw StateError('Unable to inspect PIV certificate'); } - final supported = key != null && key.public.isNotEmpty && - (key.algorithm == AlgorithmType.eccp256 || key.algorithm == AlgorithmType.rsa2048); - final compatible = supported && cert != null && pivCertificateSupportsMacos( - der: cert, - expectedPublicKey: PivPublicKey.fromSlotMetadata(key.algorithm, key.public).encodedSubjectPublicKeyInfo, - slot: id, - nowUnix: PlatformInt64Util.from(DateTime.now().millisecondsSinceEpoch ~/ 1000), + final supported = + key != null && + key.public.isNotEmpty && + (key.algorithm == AlgorithmType.eccp256 || + key.algorithm == AlgorithmType.rsa2048); + final compatible = + supported && + cert != null && + pivCertificateSupportsMacos( + der: cert, + expectedPublicKey: PivPublicKey.fromSlotMetadata( + key.algorithm, + key.public, + ).encodedSubjectPublicKeyInfo, + slot: id, + nowUnix: PlatformInt64Util.from( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + ), + ); + return PivMacOsSetupSlot( + slotNumber: slotNumber, + key: key, + certificate: cert, + compatible: compatible, ); - return PivMacOsSetupSlot(slotNumber: slotNumber, key: key, - certificate: cert, compatible: compatible); - } - - Future _readMacOsSetupSerialInSession() async { - SmartCard.assertOK(await SmartCard.transceive('00A4040005F000000000')); - final response = await SmartCard.transceive('0032000000'); - SmartCard.assertOK(response); - return SmartCard.dropSW(response).toUpperCase(); } Future inspectMacOsSetup() async { if (!supportsMetadata) return null; PivMacOsSetupPlan? plan; await SmartCard.process((_) async { - final serial = await _readMacOsSetupSerialInSession(); - await _client.select(); + await _client.prepare(); + final serial = await _client.readSerial(); final entries = []; for (final slot in ['9A', '9D']) { entries.add(await _readMacOsSetupSlotInSession(slot)); @@ -494,15 +544,22 @@ class PivController extends PollingController { plan = PivMacOsSetupPlan(serial: serial, slots: entries); for (final entry in entries) { final id = int.parse(entry.slotNumber, radix: 16); - if (entry.key == null) { slots.remove(id); } else { slots[id] = entry.key!; } + if (entry.key == null) { + slots.remove(id); + } else { + slots[id] = entry.key!; + } certificates.remove(id); certificateBytes.remove(id); certificateSlots.remove(id); if (entry.certificate != null) { certificateSlots.add(id); certificateBytes[id] = entry.certificate!; - try { certificates[id] = parseX509CertFromDer(der: entry.certificate!); } - catch (_) { /* Invalid certificates remain visible for replacement. */ } + try { + certificates[id] = parseX509CertFromDer(der: entry.certificate!); + } catch (_) { + /* Invalid certificates remain visible for replacement. */ + } } } update(); @@ -520,30 +577,51 @@ class PivController extends PollingController { required bool allowReplacement, required void Function(String slot, bool done) onProgress, }) async { - if (plan.slots.length != 2 || plan.slots[0].slotNumber != '9A' || plan.slots[1].slotNumber != '9D') return false; + if (plan.slots.length != 2 || + plan.slots[0].slotNumber != '9A' || + plan.slots[1].slotNumber != '9D') + return false; if (plan.needsReplacementConsent && !allowReplacement) return false; final actual = await inspectMacOsSetup(); if (actual == null || !plan.sameContents(actual)) return false; for (final slot in actual.slots) { - if (slot.compatible) { onProgress(slot.slotNumber, true); continue; } + if (slot.compatible) { + onProgress(slot.slotNumber, true); + continue; + } onProgress(slot.slotNumber, false); - final options = PivSelfSignOptions(slotNumber: slot.slotNumber, pinPolicy: PinPolicy.once); + final options = PivSelfSignOptions( + slotNumber: slot.slotNumber, + pinPolicy: PinPolicy.once, + ); if (slot.canReuseKey) options.algorithm = slot.key!.algorithm; options.applyMacOsLogin(); final cert = await generateSelfSignedCertificate( - slot.slotNumber, options.algorithm, + slot.slotNumber, + options.algorithm, slot.canReuseKey ? slot.key!.pinPolicy : options.pinPolicy, slot.canReuseKey ? slot.key!.touchPolicy : TouchPolicy.never, - pin, managementKey, {'CN': 'CanoKey Mac ${slot.slotNumber}'}, const [], 365, usePinOnly, - keyUsage: options.keyUsage, keyUsageCritical: options.keyUsageCritical, - extendedKeyUsage: options.extendedKeyUsage.toList(), includeBasicConstraints: true, - reuseExistingKey: slot.canReuseKey, expectedState: slot, expectedSerial: plan.serial, + pin, + managementKey, + {'CN': 'CanoKey Mac ${slot.slotNumber}'}, + const [], + 365, + usePinOnly, + keyUsage: options.keyUsage, + keyUsageCritical: options.keyUsageCritical, + extendedKeyUsage: options.extendedKeyUsage.toList(), + includeBasicConstraints: true, + reuseExistingKey: slot.canReuseKey, + expectedState: slot, + expectedSerial: plan.serial, ); if (cert == null) return false; onProgress(slot.slotNumber, true); } final verified = await inspectMacOsSetup(); - return verified != null && verified.serial == plan.serial && verified.complete; + return verified != null && + verified.serial == plan.serial && + verified.complete; } Future generateSelfSignedCertificate( @@ -567,16 +645,12 @@ class PivController extends PollingController { }) async { log.t('Call PivController.generateSelfSignedCertificate'); final c = Completer(); - final data = _generateAsymmetricKeyData(algorithm, pinPolicy, touchPolicy); - final capdu = - '004700$slot${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}${data}00'; - await SmartCard.process((String sn) async { + await _client.prepare(); if (expectedSerial != null && - expectedSerial != await _readMacOsSetupSerialInSession()) { + expectedSerial != await _client.readSerial()) { return; } - await _client.select(); if (expectedState != null) { final actual = await _readMacOsSetupSlotInSession(slot); if (!expectedState.sameContents(actual)) return; @@ -586,20 +660,36 @@ class PivController extends PollingController { return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(null); return; } PivPublicKey publicKey; if (reuseExistingKey) { final existing = await _readKeyMetadata(int.parse(slot, radix: 16)); - if (existing == null || existing.algorithm != algorithm || existing.public.isEmpty) return; - publicKey = PivPublicKey.fromSlotMetadata(existing.algorithm, existing.public); + if (existing == null || + existing.algorithm != algorithm || + existing.public.isEmpty) + return; + publicKey = PivPublicKey.fromSlotMetadata( + existing.algorithm, + existing.public, + ); } else { - final resp = await _client.transceive(capdu); - if (!SmartCard.isOK(resp)) return; - publicKey = PivPublicKey.fromGenerateResponse( - algorithm, hex.decode(SmartCard.dropSW(resp))); + try { + final spki = await _client.generateKey( + slot: int.parse(slot, radix: 16), + algorithm: algorithm.value, + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + publicKey = PivPublicKey.fromSubjectPublicKeyInfo(algorithm, spki); + } catch (_) { + return; + } } final now = DateTime.now().toUtc(); final tbsCertificate = PivCertificateBuilder.buildTbsCertificate( @@ -614,18 +704,16 @@ class PivController extends PollingController { extendedKeyUsage: extendedKeyUsage, includeBasicConstraints: includeBasicConstraints, ); - final signResp = await _generalAuthenticate( + final signature = await _signInSession( slot, algorithm, tbsCertificate, publicKey, ); - if (!SmartCard.isOK(signResp)) { + if (signature == null) { c.complete(null); return; } - final signature = - _parseAuthenticateSignature(hex.decode(SmartCard.dropSW(signResp))); final cert = PivCertificateBuilder.buildCertificate( tbsCertificate: tbsCertificate, algorithm: algorithm, @@ -642,45 +730,56 @@ class PivController extends PollingController { } Future generateKey( - String slot, - AlgorithmType algorithm, - PinPolicy pinPolicy, - TouchPolicy touchPolicy, - String pin, - String managementKey, - bool usePinOnly) async { + String slot, + AlgorithmType algorithm, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + String pin, + String managementKey, + bool usePinOnly, + ) async { log.t('Call PivController.generateKey'); final c = Completer(); - final data = _generateAsymmetricKeyData(algorithm, pinPolicy, touchPolicy); - final capdu = - '004700$slot${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}${data}00'; - - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(false); return; } - final resp = await _client.transceive(capdu); - c.complete(SmartCard.isOK(resp)); + final publicKey = await _client.generateKey( + slot: int.parse(slot, radix: 16), + algorithm: algorithm.value, + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + c.complete(publicKey.isNotEmpty); }); + if (!c.isCompleted) c.complete(false); return c.future; } - Future setPinRetries(String pin, String managementKey, - int pinRetries, int pukRetries, bool usePinOnly) async { + Future setPinRetries( + String pin, + String managementKey, + int pinRetries, + int pukRetries, + bool usePinOnly, + ) async { log.t('Call PivController.setPinRetries'); if (!supportsPinRetryConfig || pinOnlyMode || usePinOnly) { return PivPinRetryResetResult.failed; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); // Read the card as well: the UI state may be stale or the card changed. if (await _readPinOnlyModeInSession()) { c.complete(PivPinRetryResetResult.failed); @@ -691,40 +790,53 @@ class PivController extends PollingController { return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(PivPinRetryResetResult.failed); return; } - c.complete(await resetPivPinRetries( - reset: () async => SmartCard.isOK(await SmartCard.transceive( - '00FA${pinRetries.toRadixString(16).padLeft(2, '0')}' - '${pukRetries.toRadixString(16).padLeft(2, '0')}')), - authenticateManagementKey: () => - _authenticateManagementKey(managementKey), - updateMetadata: () async { - final adminData = await _getDataObject(_pivmanDataObject); - if (adminData == null) return false; - final flags = _pivmanFlags(adminData); - if (flags & _pivmanPukBlockedFlag == 0) return true; - return _putDataObject( - _pivmanDataObject, - _buildPivmanData(adminData, flags & ~_pivmanPukBlockedFlag), - ); - }, - )); + c.complete( + await resetPivPinRetries( + reset: () async { + try { + await _client.resetPinPukRetries(pinRetries, pukRetries); + return true; + } catch (_) { + return false; + } + }, + authenticateManagementKey: () => + _authenticateManagementKey(managementKey), + updateMetadata: () async { + final adminData = await _getDataObject(_pivmanDataObject); + if (adminData == null) return false; + final flags = _pivmanFlags(adminData); + if (flags & _pivmanPukBlockedFlag == 0) return true; + return _putDataObject( + _pivmanDataObject, + _buildPivmanData(adminData, flags & ~_pivmanPukBlockedFlag), + ); + }, + ), + ); }); + if (!c.isCompleted) c.complete(PivPinRetryResetResult.failed); return c.future; } Future enablePinOnlyMode( - String pin, String currentManagementKey) async { + String pin, + String currentManagementKey, + ) async { log.t('Call PivController.enablePinOnlyMode'); if (!supportsPinOnlyMode) { return false; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (!await _verifyPinInSession(pin)) { c.complete(false); return; @@ -738,8 +850,9 @@ class PivController extends PollingController { return; } final random = Random.secure(); - final newKey = - hex.encode(List.generate(24, (_) => random.nextInt(256))); + final newKey = hex.encode( + List.generate(24, (_) => random.nextInt(256)), + ); if (!await _setManagementKeyInSession( newKey, touchPolicy: managementKeyTouchPolicy, @@ -763,24 +876,32 @@ class PivController extends PollingController { } c.complete(true); }); + if (!c.isCompleted) c.complete(false); return c.future; } - Future disablePinOnlyMode(String pin, String currentManagementKey, - String newManagementKey, bool usePinOnly) async { + Future disablePinOnlyMode( + String pin, + String currentManagementKey, + String newManagementKey, + bool usePinOnly, + ) async { log.t('Call PivController.disablePinOnlyMode'); if (!supportsPinOnlyMode) { return false; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, currentManagementKey, usePinOnly)) { + pin, + currentManagementKey, + usePinOnly, + )) { c.complete(false); return; } @@ -797,60 +918,63 @@ class PivController extends PollingController { } c.complete(await _writePinOnlyObjects(newManagementKey, enabled: false)); }); + if (!c.isCompleted) c.complete(false); return c.future; } Future deriveX25519Secret( - String slot, String pin, Uint8List peerPublicKey) async { + String slot, + String pin, + Uint8List peerPublicKey, + ) async { log.t('Call PivController.deriveX25519Secret'); final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (!await _verifyPinInSession(pin)) { c.complete(null); return; } - final resp = await _generalAuthenticateRaw( - slot, - AlgorithmType.x25519, - _buildKeyAgreementData(peerPublicKey), - ); - if (!SmartCard.isOK(resp)) { + try { + c.complete( + await _client.derive( + int.parse(slot, radix: 16), + algorithmExtensionConfig.idFor(AlgorithmType.x25519), + peerPublicKey, + ), + ); + } on ProtocolException { c.complete(null); - return; } - c.complete(_parseAuthenticateSecret(hex.decode(SmartCard.dropSW(resp)))); }); + if (!c.isCompleted) c.complete(null); return c.future; } Future decapsulateMlKem768( - String slot, String pin, Uint8List ciphertext) async { + String slot, + String pin, + Uint8List ciphertext, + ) async { log.t('Call PivController.decapsulateMlKem768'); if (!supportsAlgorithm(AlgorithmType.mlkem768)) { return null; } - final request = - PivPostQuantumProtocol.buildMlKemDecapsulationData(ciphertext); final c = Completer(); try { await SmartCard.process((String sn) async { - await _client.select(); + await _client.prepare(); if (!await _verifyPinInSession(pin)) { c.complete(null); return; } - final resp = await _generalAuthenticateRaw( - slot, - AlgorithmType.mlkem768, - hex.encode(request), - ); - if (!SmartCard.isOK(resp)) { + try { + c.complete( + await _client.decapsulate(int.parse(slot, radix: 16), ciphertext), + ); + } on ProtocolException { c.complete(null); - return; } - c.complete(PivPostQuantumProtocol.parseMlKemSharedSecret( - hex.decode(SmartCard.dropSW(resp)))); }); } catch (_) { if (!c.isCompleted) { @@ -864,7 +988,11 @@ class PivController extends PollingController { } Future signData( - String slot, SlotInfo slotInfo, String pin, Uint8List data) async { + String slot, + SlotInfo slotInfo, + String pin, + Uint8List data, + ) async { log.t('Call PivController.signData'); final publicKey = publicKeyForSlot(slotInfo); if (publicKey == null) { @@ -874,19 +1002,17 @@ class PivController extends PollingController { try { await SmartCard.process((String sn) async { try { - await _client.select(); + await _client.prepare(); if (!await _verifyPinInSession(pin)) { c.complete(null); return; } - final resp = await _generalAuthenticate( - slot, slotInfo.algorithm, data, publicKey); - if (!SmartCard.isOK(resp)) { - c.complete(null); - return; - } - final signature = - _parseAuthenticateSignature(hex.decode(SmartCard.dropSW(resp))); + final signature = await _signInSession( + slot, + slotInfo.algorithm, + data, + publicKey, + ); c.complete(signature); } catch (_) { if (!c.isCompleted) { @@ -906,7 +1032,10 @@ class PivController extends PollingController { } Future verifySignature( - SlotInfo slotInfo, Uint8List data, Uint8List signature) async { + SlotInfo slotInfo, + Uint8List data, + Uint8List signature, + ) async { log.t('Call PivController.verifySignature'); final publicKey = publicKeyForSlot(slotInfo); if (publicKey == null) { @@ -923,27 +1052,31 @@ class PivController extends PollingController { } } - Future importPostQuantumSeed(String slotNumber, AlgorithmType algorithm, - Uint8List seed, PinPolicy pinPolicy, TouchPolicy touchPolicy) async { + Future importPostQuantumSeed( + String slotNumber, + AlgorithmType algorithm, + Uint8List seed, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + ) async { log.t('Call PivController.importPostQuantumSeed'); - if (!supportsAlgorithm(algorithm)) { - return false; - } - try { - PivPostQuantumProtocol.buildImportData( - algorithm: algorithm, - seed: seed, - pinPolicy: pinPolicy, - touchPolicy: touchPolicy, - ); - } on ArgumentError { + if (!supportsAlgorithm(algorithm) || + !_isValidPostQuantumSeed(algorithm, seed)) { return false; } final c = Completer(); try { await SmartCard.process((String sn) async { - c.complete(await _importPostQuantumSeedInSession( - slotNumber, algorithm, seed, pinPolicy, touchPolicy)); + await _client.prepare(); + c.complete( + await _importPostQuantumSeedInSession( + slotNumber, + algorithm, + seed, + pinPolicy, + touchPolicy, + ), + ); }); } catch (_) { if (!c.isCompleted) { @@ -964,25 +1097,30 @@ class PivController extends PollingController { }) async { log.t('Call PivController.changeAlgorithmExtensionConfigAuthenticated'); final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(false); return; } - final data = hex.encode(config.encode()); - final resp = await SmartCard.transceive( - '00EE0200${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}$data'); - if (SmartCard.isOK(resp)) { - algorithmExtensionConfig = config; + try { + await _client.setAlgorithmConfig(Uint8List.fromList(config.encode())); + } on ProtocolException { + c.complete(false); + return; } - c.complete(SmartCard.isOK(resp)); + algorithmExtensionConfig = config; + c.complete(true); }); + if (!c.isCompleted) c.complete(false); return c.future; } @@ -1000,56 +1138,57 @@ class PivController extends PollingController { }) async { log.t('Call PivController.importAuthenticated'); if ((mlDsaSeed != null && !supportsAlgorithm(AlgorithmType.mldsa65)) || - (mlKemSeed != null && !supportsAlgorithm(AlgorithmType.mlkem768))) { - return false; - } - try { - if (mlDsaSeed != null) { - PivPostQuantumProtocol.buildImportData( - algorithm: AlgorithmType.mldsa65, - seed: mlDsaSeed, - pinPolicy: pinPolicy, - touchPolicy: touchPolicy, - ); - } - if (mlKemSeed != null) { - PivPostQuantumProtocol.buildImportData( - algorithm: AlgorithmType.mlkem768, - seed: mlKemSeed, - pinPolicy: pinPolicy, - touchPolicy: touchPolicy, - ); - } - } on ArgumentError { + (mlKemSeed != null && !supportsAlgorithm(AlgorithmType.mlkem768)) || + (mlDsaSeed != null && + !_isValidPostQuantumSeed(AlgorithmType.mldsa65, mlDsaSeed)) || + (mlKemSeed != null && + !_isValidPostQuantumSeed(AlgorithmType.mlkem768, mlKemSeed))) { return false; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(false); return; } if (privateKey != null && !await _importPrivateKeyInSession( - slot, privateKey, pinPolicy, touchPolicy)) { + slot, + privateKey, + pinPolicy, + touchPolicy, + )) { c.complete(false); return; } if (mlDsaSeed != null && !await _importPostQuantumSeedInSession( - slot, AlgorithmType.mldsa65, mlDsaSeed, pinPolicy, touchPolicy)) { + slot, + AlgorithmType.mldsa65, + mlDsaSeed, + pinPolicy, + touchPolicy, + )) { c.complete(false); return; } if (mlKemSeed != null && - !await _importPostQuantumSeedInSession(slot, AlgorithmType.mlkem768, - mlKemSeed, pinPolicy, touchPolicy)) { + !await _importPostQuantumSeedInSession( + slot, + AlgorithmType.mlkem768, + mlKemSeed, + pinPolicy, + touchPolicy, + )) { c.complete(false); return; } @@ -1059,118 +1198,135 @@ class PivController extends PollingController { } c.complete(true); }); + if (!c.isCompleted) c.complete(false); return c.future; } Future _importPrivateKeyInSession( - String slotNumber, - PivPrivateKeyData key, - PinPolicy pinPolicy, - TouchPolicy touchPolicy) async { + String slotNumber, + PivPrivateKeyData key, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + ) async { final algorithm = AlgorithmType.fromValue(key.algorithm); - final data = [ - ...key.importData, - 0xAA, - 0x01, - pinPolicy.value, - 0xAB, - 0x01, - touchPolicy.value, - ]; - final resp = await _sendChainedData( - instruction: 'FE', - p1p2: '${_algorithmIdHex(algorithm)}$slotNumber', - data: hex.encode(data), - ); - return SmartCard.isOK(resp); + final slot = int.parse(slotNumber, radix: 16); + final wireId = algorithmExtensionConfig.idFor(algorithm); + final components = TLV.parse(key.importData); + final copies = []; + Uint8List component(int tag) { + final value = components[tag]; + if (value is! List) { + throw FormatException( + 'PIV import data lacks component ${tag.toRadixString(16)}', + ); + } + final copy = Uint8List.fromList(value); + copies.add(copy); + return copy; + } + + try { + switch (algorithm) { + case AlgorithmType.rsa1024 || + AlgorithmType.rsa2048 || + AlgorithmType.rsa3072 || + AlgorithmType.rsa4096: + await _client.importRsaKey( + slot: slot, + algorithm: wireId, + p: component(0x01), + q: component(0x02), + dp: component(0x03), + dq: component(0x04), + qinv: component(0x05), + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + case AlgorithmType.ed25519: + await _client.importEd25519Key( + slot: slot, + seed: component(0x06), + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + default: + await _client.importEcKey( + slot: slot, + algorithm: wireId, + scalar: component(0x06), + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + } + return true; + } on ProtocolException { + return false; + } on FormatException { + return false; + } finally { + for (final copy in copies) { + copy.fillRange(0, copy.length, 0); + } + } } Future _importPostQuantumSeedInSession( - String slotNumber, - AlgorithmType algorithm, - Uint8List seed, - PinPolicy pinPolicy, - TouchPolicy touchPolicy) async { - final data = PivPostQuantumProtocol.buildImportData( - algorithm: algorithm, - seed: seed, - pinPolicy: pinPolicy, - touchPolicy: touchPolicy, - ); - final resp = await _sendChainedData( - instruction: 'FE', - p1p2: '${_algorithmIdHex(algorithm)}$slotNumber', - data: hex.encode(data), - ); - return SmartCard.isOK(resp); + String slotNumber, + AlgorithmType algorithm, + Uint8List seed, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + ) async { + final kind = switch (algorithm) { + AlgorithmType.mldsa65 => 0, + AlgorithmType.mlkem768 => 1, + _ => null, + }; + if (kind == null || !_isValidPostQuantumSeed(algorithm, seed)) { + return false; + } + final copy = Uint8List.fromList(seed); + try { + await _client.importPqSeed( + slot: int.parse(slotNumber, radix: 16), + kind: kind, + seed: copy, + pinPolicy: pinPolicy.value, + touchPolicy: touchPolicy.value, + ); + return true; + } on ProtocolException { + return false; + } finally { + copy.fillRange(0, copy.length, 0); + } } + /// Normalized certificate object value; libcanokey adds the outer 53 + /// container and the PUT DATA framing/segmentation. Uint8List buildPivCert(Uint8List cert) { - // Create a builder for the cert tag (0x70) - var certTlv = Uint8List(2 + 2 + cert.length); - certTlv[0] = 0x70; - certTlv[1] = 0x82; - certTlv[2] = (cert.length >> 8) & 0xFF; - certTlv[3] = cert.length & 0xFF; - certTlv.setRange(4, 4 + cert.length, cert); - - // Add the compressed tag (0x71) - var compressedTlv = Uint8List(3); - compressedTlv[0] = 0x71; - compressedTlv[1] = 0x01; - compressedTlv[2] = 0x00; - - // Add the LRC tag (0xFE) - var lrcTlv = Uint8List(2); - lrcTlv[0] = 0xFE; - lrcTlv[1] = 0x00; - - // Calculate total length - var totalLen = certTlv.length + compressedTlv.length + lrcTlv.length; - - // Create the final buffer with compact tag (0x53) - var result = Uint8List(2 + 2 + totalLen); - result[0] = 0x53; - result[1] = 0x82; - result[2] = (totalLen >> 8) & 0xFF; - result[3] = totalLen & 0xFF; - - // Copy all TLVs into the result - result.setRange(4, 4 + certTlv.length, certTlv); - result.setRange(4 + certTlv.length, - 4 + certTlv.length + compressedTlv.length, compressedTlv); - result.setRange( - 4 + certTlv.length + compressedTlv.length, result.length, lrcTlv); - - return result; + return Uint8List.fromList([ + 0x70, 0x82, (cert.length >> 8) & 0xFF, cert.length & 0xFF, + ...cert, + 0x71, 0x01, 0x00, // uncompressed + 0xFE, 0x00, // empty LRC + ]); } Future _importCertInSession(String slot, Uint8List cert) async { - int slotInt = int.parse(slot, radix: 16); - if (_certDO.containsKey(slotInt)) { - cert = - cert.isEmpty ? Uint8List.fromList([0x53, 0x00]) : buildPivCert(cert); - String data = - '5C035FC1${hex.encode([_certDO[slotInt]!])}${hex.encode(cert)}'; - const int chunkSize = 0xFF * 2; - int offset = 0; - while (offset < data.length) { - int chunkLength = min(chunkSize, data.length - offset); - String cla = '10'; - if (offset + chunkLength == data.length) cla = '00'; - int dataSize = chunkLength ~/ 2; - String lc = dataSize.toRadixString(16).padLeft(2, '0'); - String capdu = - '${cla}DB3FFF$lc${data.substring(offset, offset + chunkLength)}'; - String resp = await SmartCard.transceive(capdu); - if (!SmartCard.isOK(resp)) { - return false; - } - offset += chunkLength; - } + final objectId = _certDO[int.parse(slot, radix: 16)]; + if (objectId == null) { + return false; + } + try { + await _client.writeObject( + 0x5FC100 + objectId, + cert.isEmpty ? Uint8List(0) : buildPivCert(cert), + ); return true; + } on ProtocolException { + return false; } - return false; } Future clearSlotAuthenticated({ @@ -1184,24 +1340,29 @@ class PivController extends PollingController { return false; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(false); return; } - final deleteKeyResp = await SmartCard.transceive('00F6FF$slot'); - if (!SmartCard.isOK(deleteKeyResp)) { + try { + await _client.deleteKey(int.parse(slot, radix: 16)); + } catch (_) { c.complete(false); return; } c.complete(await _importCertInSession(slot, Uint8List(0))); }); + if (!c.isCompleted) c.complete(false); return c.future; } @@ -1217,21 +1378,31 @@ class PivController extends PollingController { return false; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); if (usePinOnly && !await _verifyPinInSession(pin)) { c.complete(false); return; } if (!await _authenticateManagementKeyOrPinOnly( - pin, managementKey, usePinOnly)) { + pin, + managementKey, + usePinOnly, + )) { c.complete(false); return; } - final resp = await SmartCard.transceive( - '00F6${targetSlot.toUpperCase()}${sourceSlot.toUpperCase()}'); - c.complete(SmartCard.isOK(resp)); + try { + await _client.moveKey( + int.parse(sourceSlot, radix: 16), + int.parse(targetSlot, radix: 16), + ); + c.complete(true); + } catch (_) { + c.complete(false); + } }); + if (!c.isCompleted) c.complete(false); return c.future; } @@ -1241,15 +1412,15 @@ class PivController extends PollingController { return null; } final c = Completer(); - SmartCard.process((String sn) async { - await _client.select(); - final resp = await _client.transceive('00F9${slot.toUpperCase()}0000'); - if (!SmartCard.isOK(resp)) { + await SmartCard.process((String sn) async { + await _client.prepare(); + try { + c.complete(await _client.attest(int.parse(slot, radix: 16))); + } on ProtocolException { c.complete(null); - return; } - c.complete(Uint8List.fromList(hex.decode(SmartCard.dropSW(resp)))); }); + if (!c.isCompleted) c.complete(null); return c.future; } @@ -1261,127 +1432,52 @@ class PivController extends PollingController { return BigInt.parse(hex.encode(bytes), radix: 16); } - String _buildAuthenticateData( - AlgorithmType algorithm, Uint8List data, PivPublicKey? publicKey) { - final input = preparePivSigningInput( - algorithm: algorithm.value, - data: data, - publicKey: publicKey?.rawPublicKey, - ); - final inner = '820081${_hexLength(input.length)}${hex.encode(input)}'; - return '7C${_hexLength(inner.length ~/ 2)}$inner'; - } - - Future _generalAuthenticate(String slot, AlgorithmType algorithm, - Uint8List data, PivPublicKey? publicKey) async { - final signData = _buildAuthenticateData(algorithm, data, publicKey); - final algorithmId = PivSignatureProtocol.generalAuthenticateAlgorithmId( - algorithm: algorithm, - messageLength: data.length, - config: algorithmExtensionConfig, - ); - return _generalAuthenticateRaw( - slot, - algorithm, - signData, - algorithmId: algorithmId, - ); - } - - Future _generalAuthenticateRaw( - String slot, AlgorithmType algorithm, String data, - {int? algorithmId}) async { - final algorithmHex = - (algorithmId ?? algorithmExtensionConfig.idFor(algorithm)) - .toRadixString(16) - .padLeft(2, '0'); - return _sendChainedData( - p1p2: '$algorithmHex$slot', - instruction: '87', - data: data, - le: '00', - ); - } - - Future _sendChainedData({ - required String instruction, - required String p1p2, - required String data, - String le = '', - }) async { - const chunkSize = 0xFF * 2; - var offset = 0; - String resp = ''; - while (offset < data.length) { - final chunkLength = min(chunkSize, data.length - offset); - final last = offset + chunkLength == data.length; - final cla = last ? '00' : '10'; - final chunk = data.substring(offset, offset + chunkLength); - final lc = (chunk.length ~/ 2).toRadixString(16).padLeft(2, '0'); - resp = - await _client.transceive('$cla$instruction$p1p2$lc$chunk${last ? le : ''}'); - if (!SmartCard.isOK(resp)) { - return resp; - } - offset += chunkLength; - } - return resp; - } - - String _algorithmIdHex(AlgorithmType algorithm) { - return algorithmExtensionConfig - .idFor(algorithm) - .toRadixString(16) - .padLeft(2, '0'); - } - - String _generateAsymmetricKeyData( - AlgorithmType algorithm, PinPolicy pinPolicy, TouchPolicy touchPolicy) { - final inner = '8001${_algorithmIdHex(algorithm)}' - 'AA01${pinPolicy.value.toRadixString(16).padLeft(2, '0')}' - 'AB01${touchPolicy.value.toRadixString(16).padLeft(2, '0')}'; - return 'AC${_hexLength(inner.length ~/ 2)}$inner'; - } - - Uint8List _parseAuthenticateSignature(List data) { - final map = TLV.parse(data); - final dynamic inner = map[0x7C]; - if (inner is List) { - final innerMap = TLV.parse(inner); - final signature = innerMap[0x82]; - if (signature is List) { - return Uint8List.fromList(signature); - } - } - final signature = map[0x82]; - if (signature is List) { - return Uint8List.fromList(signature); - } - throw ArgumentError('Invalid signature response'); - } - - Uint8List _parseAuthenticateSecret(List data) { - final map = TLV.parse(data); - final dynamic inner = map[0x7C]; - if (inner is List) { - final innerMap = TLV.parse(inner); - final secret = innerMap[0x82] ?? innerMap[0x86]; - if (secret is List) { - return Uint8List.fromList(secret); - } - } - final secret = map[0x82] ?? map[0x86]; - if (secret is List) { - return Uint8List.fromList(secret); + /// Sign through libcanokey; host-side digest/padding stays here. ML-DSA-65 + /// messages and Ed25519 messages outside the classic input budget use the + /// firmware streaming modes; SM2 keeps the host-computed ZA digest. + Future _signInSession( + String slot, + AlgorithmType algorithm, + Uint8List data, + PivPublicKey? publicKey, + ) async { + final slotId = int.parse(slot, radix: 16); + try { + if (algorithm == AlgorithmType.mldsa65) { + return await _client.signStreaming(slot: slotId, mode: 0, message: data); + } + if (algorithm == AlgorithmType.ed25519 && + (data.isEmpty || data.length > 512)) { + // Randomized Ed25519; the signature still verifies as plain Ed25519. + return await _client.signStreaming(slot: slotId, mode: 1, message: data); + } + return await _client.sign( + slot: slotId, + algorithm: algorithmExtensionConfig.idFor(algorithm), + input: preparePivSigningInput( + algorithm: algorithm.value, + data: data, + publicKey: publicKey?.rawPublicKey, + ), + inputKind: switch (algorithm) { + AlgorithmType.rsa1024 || + AlgorithmType.rsa2048 || + AlgorithmType.rsa3072 || + AlgorithmType.rsa4096 => 0, + AlgorithmType.ed25519 => 2, + _ => 1, + }, + ); + } on ProtocolException { + return null; + } on String { + return null; } - throw ArgumentError('Invalid key agreement response'); } - String _buildKeyAgreementData(Uint8List peerPublicKey) { - final inner = '8500' - '81${_hexLength(peerPublicKey.length)}${hex.encode(peerPublicKey)}'; - return '7C${_hexLength(inner.length ~/ 2)}$inner'; - } + bool _isValidPostQuantumSeed(AlgorithmType algorithm, Uint8List seed) => + (algorithm == AlgorithmType.mldsa65 && seed.length == 32) || + (algorithm == AlgorithmType.mlkem768 && seed.length == 64); Future _verifyPinInSession(String pin) async { final ok = await _client.verifyPin(pin); @@ -1392,7 +1488,10 @@ class PivController extends PollingController { } Future _authenticateManagementKeyOrPinOnly( - String pin, String managementKey, bool usePinOnly) async { + String pin, + String managementKey, + bool usePinOnly, + ) async { if (usePinOnly) { final protectedKey = await _readProtectedManagementKeyInSession(); if (protectedKey == null) { @@ -1413,22 +1512,7 @@ class PivController extends PollingController { if (key.length != 48 || !RegExp(r'^[0-9a-fA-F]+$').hasMatch(key)) { return false; } - final algorithm = managementKeyAlgorithm; - String resp = await SmartCard.transceive( - PivManagementKeyProtocol.authenticateRequest(algorithm)); - SmartCard.assertOK(resp); - final challenge = PivManagementKeyProtocol.parseChallenge( - hex.decode(SmartCard.dropSW(resp)), - algorithm, - ); - final auth = PivManagementKeyProtocol.encryptChallenge( - algorithm: algorithm, - key: hex.decode(key), - challenge: challenge, - ); - resp = await SmartCard.transceive( - PivManagementKeyProtocol.authenticateResponse(algorithm, auth)); - return SmartCard.isOK(resp); + return _client.authenticateManagementKey(key, managementKeyAlgorithm); } Future _setManagementKeyInSession( @@ -1438,14 +1522,22 @@ class PivController extends PollingController { if (key.length != 48 || !RegExp(r'^[0-9a-fA-F]+$').hasMatch(key)) { return false; } - final resp = await SmartCard.transceive( - PivManagementKeyProtocol.setManagementKey( - algorithm: managementKeyAlgorithm, - key: key, - touchPolicy: touchPolicy, - ), - ); - return SmartCard.isOK(resp); + final bytes = Uint8List.fromList(hex.decode(key)); + try { + await _client.setManagementKey( + algorithm: managementKeyAlgorithm.value, + key: bytes, + touch: managementKeyAlgorithm == AlgorithmType.aes192 && + touchPolicy == TouchPolicy.always + ? 1 + : 0, + ); + return true; + } on ProtocolException { + return false; + } finally { + bytes.fillRange(0, bytes.length, 0); + } } AlgorithmType get managementKeyAlgorithm => @@ -1486,13 +1578,15 @@ class PivController extends PollingController { return null; } - Future _writePinOnlyObjects(String managementKey, - {required bool enabled}) async { + Future _writePinOnlyObjects( + String managementKey, { + required bool enabled, + }) async { final adminData = await _getDataObject(_pivmanDataObject); final newFlags = enabled ? _pivmanFlags(adminData) | - _pivmanManagementKeyProtectedFlag | - _pivmanPukBlockedFlag + _pivmanManagementKeyProtectedFlag | + _pivmanPukBlockedFlag : _pivmanFlags(adminData) & ~_pivmanManagementKeyProtectedFlag; final adminOk = await _putDataObject( _pivmanDataObject, @@ -1569,38 +1663,12 @@ class PivController extends PollingController { return Uint8List.fromList(hex.decode(_tlv(_pivmanDataTag, inner))); } - Future?> _getDataObject(int objectId) async { - final resp = await _client.transceive( - '00CB3FFF055C03${objectId.toRadixString(16).padLeft(6, '0')}00'); - if (!SmartCard.isOK(resp)) { - return null; - } - final data = hex.decode(SmartCard.dropSW(resp)); - if (data.isEmpty) { - return data; - } - try { - final parsed = TLV.parse(data); - final objectData = parsed[_objectDataTag]; - if (objectData is List) { - return objectData; - } - } catch (_) { - return data; - } - return data; - } + Future?> _getDataObject(int objectId) => + _client.readObject(objectId); Future _putDataObject(int objectId, Uint8List data) async { - final objectIdHex = objectId.toRadixString(16).padLeft(6, '0'); - final body = '5C03$objectIdHex' - '${_tlv(_objectDataTag, data)}'; - final resp = await _sendChainedData( - instruction: 'DB', - p1p2: '3FFF', - data: body, - ); - return SmartCard.isOK(resp); + await _client.writeObject(objectId, data); + return true; } String _tlv(int tag, List value) { @@ -1628,20 +1696,19 @@ class PivController extends PollingController { }; List get _keySlots => [ - 0x9A, - 0x9C, - 0x9D, - 0x9E, - ...(extendedRetiredSlots ? _extendedRetiredSlots : _legacyRetiredSlots), - ]; + 0x9A, + 0x9C, + 0x9D, + 0x9E, + ...(extendedRetiredSlots ? _extendedRetiredSlots : _legacyRetiredSlots), + ]; final List _legacyRetiredSlots = [0x82, 0x83]; final List _extendedRetiredSlots = [ - for (var slot = 0x82; slot <= 0x95; slot++) slot + for (var slot = 0x82; slot <= 0x95; slot++) slot, ]; - static const int _objectDataTag = 0x53; static const int _pivmanDataObject = 0x5FFF00; static const int _pivmanProtectedDataObject = 0x5FC109; static const int _pivmanDataTag = 0x80; diff --git a/lib/controller/applets/settings/settings_controller.dart b/lib/controller/applets/settings/settings_controller.dart index 56bd30b..14d6202 100644 --- a/lib/controller/applets/settings/settings_controller.dart +++ b/lib/controller/applets/settings/settings_controller.dart @@ -1,6 +1,6 @@ import 'package:canokey_console/helper/utils/applet_switches.dart'; +import 'package:canokey_console/helper/storage/local_storage.dart'; import 'dart:async'; -import 'dart:typed_data'; import 'package:canokey_console/controller/base/admin.dart'; import 'package:canokey_console/controller/base/polling_controller.dart'; @@ -10,6 +10,7 @@ import 'package:canokey_console/helper/theme/admin_theme.dart'; import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/screenshot_mode.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/canokey.dart'; @@ -21,9 +22,15 @@ import 'package:logger/logger.dart'; class SettingsController extends PollingController with AdminApplet { SettingsController({AdminCardClient? client}) - : _client = client ?? AdminCardClient(); + : _client = client ?? AdminCardClient(); final AdminCardClient _client; + @override + AdminCardClient get adminCardClient => _client; + + @override + bool get refreshWebOnReady => false; + late CanoKey key; @override @@ -63,28 +70,80 @@ class SettingsController extends PollingController with AdminApplet { return; } + final pin = adminPinForCurrentLease; final featureValues = {}; + final standard = { + Func.led, + Func.ndefReadonly, + Func.ndefEnabled, + Func.webusbLandingPage, + }; + final known = { + ...standard, + ...AppletSwitches.featureBits.keys, + Func.hotp, + Func.keyboardWithReturn, + Func.sigTouch, + Func.decTouch, + Func.autTouch, + Func.nfcSwitch, + }; + if (values.keys.any((key) => !known.contains(key))) { + throw ArgumentError('Unsupported Admin switch'); + } for (final entry in values.entries) { if (AppletSwitches.featureBits.containsKey(entry.key)) { featureValues[entry.key] = entry.value; - } else { - SmartCard.assertOK(await SmartCard.transceive( - _changeSwitchAPDUs[entry.key]![entry.value]!)); } } - - if (featureValues.isNotEmpty) { - SmartCard.assertOK(await SmartCard.transceive( - _changeFeatureSwitchesAPDU(featureValues))); + if (values.keys.any(standard.contains) || featureValues.isNotEmpty) { + final mask = featureValues.keys.fold( + 0, + (mask, key) => mask | AppletSwitches.featureBits[key]!, + ); + await _client.configure( + pin: pin, + ledOn: values[Func.led], + ndefReadOnly: values[Func.ndefReadonly], + ndefEnabled: values[Func.ndefEnabled], + webusbLanding: values[Func.webusbLandingPage], + featureMask: mask, + featureValues: AppletSwitches.updateFeatureMask(0, featureValues), + ); + await _client.prepare(); + } + for (final entry in values.entries) { + switch (entry.key) { + case Func.hotp: + await _client.setKeyboardInterface(entry.value, pin: pin); + case Func.keyboardWithReturn: + await _client.setKeyboardReturn(entry.value, pin: pin); + case Func.sigTouch: + await _client.setLegacyTouch(0, entry.value ? 1 : 0, pin: pin); + case Func.decTouch: + await _client.setLegacyTouch(1, entry.value ? 1 : 0, pin: pin); + case Func.autTouch: + await _client.setLegacyTouch(2, entry.value ? 1 : 0, pin: pin); + case Func.nfcSwitch: + await _client.setNfcEnabled(entry.value, pin: pin); + default: + continue; + } + // Every successful profile-affecting operation ends its own auth/target + // sequence. Explicitly discover before the next operation, never inside it. + await _client.prepare(); } log.i( - 'Successfully changed switches: ${values.keys.map((e) => e.name).join(', ')}'); + 'Successfully changed switches: ${values.keys.map((e) => e.name).join(', ')}', + ); Navigator.pop(Get.context!); Prompts.showPrompt( - S.of(Get.context!).successfullyChanged, ContentThemeColor.success, - forceSnackBar: true); + S.of(Get.context!).successfullyChanged, + ContentThemeColor.success, + forceSnackBar: true, + ); await _refresh(sn); }); } @@ -96,13 +155,18 @@ class SettingsController extends PollingController with AdminApplet { return; } - await _client.changePin(newPin); + final oldPin = adminPinForCurrentLease; + // A lost acknowledgment may mean the new PIN is already committed. + await forgetAdminPin(sn); + await _client.changePin(newPin, currentPin: oldPin); log.i('Successfully changed PIN'); Navigator.pop(Get.context!); Prompts.showPrompt( - S.of(Get.context!).pinChanged, ContentThemeColor.success, - forceSnackBar: true); + S.of(Get.context!).pinChanged, + ContentThemeColor.success, + forceSnackBar: true, + ); await updatePinCache(sn, newPin, savePin); }); @@ -115,36 +179,62 @@ class SettingsController extends PollingController with AdminApplet { return; } - SmartCard.assertOK(await SmartCard.transceive(applet.resetApdu)); + // Clearing first covers committed resets with lost acknowledgments too. + if (applet == Applet.oath) { + await LocalStorage.setPinCache(sn, 'OATH', null); + } + if (applet == Applet.webauthn) { + await LocalStorage.setPinCache(sn, 'webauthn', null); + } + await _client.resetApplet(applet, pin: adminPinForCurrentLease); log.i('Successfully reset ${applet.name}'); Navigator.pop(Get.context!); Prompts.showPrompt( - S.of(Get.context!).settingsResetSuccess, ContentThemeColor.success, - forceSnackBar: true); + S.of(Get.context!).settingsResetSuccess, + ContentThemeColor.success, + forceSnackBar: true, + ); }); } - void resetCanokey() { + void resetCanokey() async { log.t('Call SettingsController.resetCanokey'); - SmartCard.process((String sn) async { - await _client.select(); + await SmartCard.process((String sn) async { + await _client.prepare(); AppLoaderOverlay.show(); - String resp = await SmartCard.transceive('00500000055245534554'); - AppLoaderOverlay.hide(); + int? status; + try { + await LocalStorage.clearPinCacheForDevice(sn); + await _client.factoryReset(); + status = 0x9000; + } on ProtocolException catch (error) { + status = error.details.statusWord; + } finally { + AppLoaderOverlay.hide(); + await forgetAdminPin(sn); + } Navigator.pop(Get.context!); - if (resp == '9000') { + if (status == 0x9000) { + Prompts.showPrompt( + S.of(Get.context!).settingsResetSuccess, + ContentThemeColor.success, + ); + } else if (status == 0x6985) { Prompts.showPrompt( - S.of(Get.context!).settingsResetSuccess, ContentThemeColor.success); - } else if (resp == '6985') { + S.of(Get.context!).settingsResetConditionNotSatisfying, + ContentThemeColor.danger, + ); + } else if (status == 0x6982) { Prompts.showPrompt( - S.of(Get.context!).settingsResetConditionNotSatisfying, - ContentThemeColor.danger); - } else if (resp == '6982') { - Prompts.showPrompt(S.of(Get.context!).settingsResetPresenceTestFailed, - ContentThemeColor.danger); + S.of(Get.context!).settingsResetPresenceTestFailed, + ContentThemeColor.danger, + ); } else { - Prompts.showPrompt(S.current.settingsResetFailed, ContentThemeColor.danger); + Prompts.showPrompt( + S.current.settingsResetFailed, + ContentThemeColor.danger, + ); } }); } @@ -157,7 +247,7 @@ class SettingsController extends PollingController with AdminApplet { } if (preset.isDefault) { - SmartCard.assertOK(await SmartCard.transceive('00470000')); + await _client.clearKeyboardKeymap(pin: adminPinForCurrentLease); } else { final entries = preset.entries; final id = preset.id; @@ -168,20 +258,29 @@ class SettingsController extends PollingController with AdminApplet { if (validationError != null) { throw Exception(validationError); } - SmartCard.assertOK(await SmartCard.transceive( - '004500${id.toRadixString(16).padLeft(2, '0')}000100${hex.encode(entries)}')); + await _client.writeKeyboardKeymap( + id, + entries, + pin: adminPinForCurrentLease, + ); } log.i('Successfully changed keyboard layout'); Navigator.pop(Get.context!); Prompts.showPrompt( - S.of(Get.context!).successfullyChanged, ContentThemeColor.success, - forceSnackBar: true); + S.of(Get.context!).successfullyChanged, + ContentThemeColor.success, + forceSnackBar: true, + ); await _refresh(sn); }); } Future _refresh(String sn) async { + // Authentication already discovered and verified this lease; re-probe only + // when writes invalidated the profile evidence. + await _client.prepareIfStale(); + final pin = adminPinForCurrentLease; final firmwareVersion = await _client.readFirmwareVersion(); String? coreCommit; final model = await _client.readModel(); @@ -194,7 +293,7 @@ class SettingsController extends PollingController with AdminApplet { if (functionSetVersion == FunctionSetVersion.v5) { coreCommit = await _client.readCoreCommit(); } - final config = await _client.readConfig(); + final config = await _client.readConfig(pin: pin); final switches = AppletSwitchStatus.fromConfig( firmwareVersion: FirmwareVersion.parse(firmwareVersion), functionSetVersion: functionSetVersion, @@ -202,7 +301,8 @@ class SettingsController extends PollingController with AdminApplet { ); final legacy = functionSetVersion == FunctionSetVersion.v1; final ledOn = config[0] == 1; - final hotpOn = functionSetVersion.index <= FunctionSetVersion.v3.index && + final hotpOn = + functionSetVersion.index <= FunctionSetVersion.v3.index && config[1] == 1; final ndefReadonly = config[2] == 1; final ndefEnabled = !legacy && config[3] == 1; @@ -217,11 +317,11 @@ class SettingsController extends PollingController with AdminApplet { StorageUsage? storageUsage; KeyboardKeymapState? keyboardKeymap; if (functionSet.contains(Func.nfcSwitch)) { - nfcEnabled = await _client.readNfcEnabled(); + nfcEnabled = await _client.readNfcEnabled(pin: pin); } if (functionSet.contains(Func.dynamicOathCapacity) || functionSet.contains(Func.dynamicWebAuthnCapacity)) { - final totalUsage = await _client.readStorageUsage(); + final totalUsage = await _client.readStorageUsage(pin: pin); storageUsage = StorageUsage( usedKiB: totalUsage.usedKiB, totalKiB: totalUsage.totalKiB, @@ -236,36 +336,37 @@ class SettingsController extends PollingController with AdminApplet { } } if (functionSet.contains(Func.keyboardKeymap)) { - keyboardKeymap = await _tryReadKeyboardKeymap(); + keyboardKeymap = await _readKeyboardKeymap(pin); } key = CanoKey( - model: model, - sn: sn, - chipId: chipId, - firmwareVersion: firmwareVersion, - coreCommit: coreCommit, - functionSetVersion: functionSetVersion, - ledOn: ledOn, - hotpOn: hotpOn, - ndefReadonly: ndefReadonly, - ndefEnabled: ndefEnabled, - webusbLandingEnabled: webusbLandingEnabled, - keyboardWithReturn: keyboardWithReturn, - sigTouch: sigTouch, - decTouch: decTouch, - autTouch: autTouch, - touchCacheTime: cacheTime, - nfcEnabled: nfcEnabled, - passEnabled: switches.passEnabled, - openPgpCcIdEnabled: switches.openPgpUsbEnabled, - openPgpNfcEnabled: switches.openPgpNfcEnabled, - pivCcIdEnabled: switches.pivUsbEnabled, - pivNfcEnabled: switches.pivNfcEnabled, - webAuthnEnabled: switches.webAuthnEnabled, - featureSwitchesSupported: switches.featureSwitchesSupported, - storageUsage: storageUsage, - keyboardKeymap: keyboardKeymap); + model: model, + sn: sn, + chipId: chipId, + firmwareVersion: firmwareVersion, + coreCommit: coreCommit, + functionSetVersion: functionSetVersion, + ledOn: ledOn, + hotpOn: hotpOn, + ndefReadonly: ndefReadonly, + ndefEnabled: ndefEnabled, + webusbLandingEnabled: webusbLandingEnabled, + keyboardWithReturn: keyboardWithReturn, + sigTouch: sigTouch, + decTouch: decTouch, + autTouch: autTouch, + touchCacheTime: cacheTime, + nfcEnabled: nfcEnabled, + passEnabled: switches.passEnabled, + openPgpCcIdEnabled: switches.openPgpUsbEnabled, + openPgpNfcEnabled: switches.openPgpNfcEnabled, + pivCcIdEnabled: switches.pivUsbEnabled, + pivNfcEnabled: switches.pivNfcEnabled, + webAuthnEnabled: switches.webAuthnEnabled, + featureSwitchesSupported: switches.featureSwitchesSupported, + storageUsage: storageUsage, + keyboardKeymap: keyboardKeymap, + ); polled = true; @@ -273,18 +374,14 @@ class SettingsController extends PollingController with AdminApplet { } Future> _tryReadAppletStorageUsage( - Set functionSet) async { - final resp = await SmartCard.transceive('0041010030'); - if (!SmartCard.isOK(resp)) { - log.w('Failed to read applet flash usage: $resp'); - return []; - } - - final data = SmartCard.dropSW(resp); - if (data.length < _appletUsageMinResponseLengthHex || - data.length % _appletUsageRecordLengthHex != 0) { - log.w('Invalid applet flash usage length: ${data.length ~/ 2}'); - return []; + Set functionSet, + ) async { + String data; + try { + data = hex.encode(await _client.readAppletStorageUsage()); + } on ProtocolException catch (error) { + if (error.details.kind == 'UnsupportedFeature') return []; + rethrow; } final usages = []; @@ -292,72 +389,55 @@ class SettingsController extends PollingController with AdminApplet { for (var i = 0; i < recordCount; i++) { final offset = i * _appletUsageRecordLengthHex; final id = int.parse(data.substring(offset, offset + 2), radix: 16); - final flags = - int.parse(data.substring(offset + 2, offset + 4), radix: 16); + final flags = int.parse( + data.substring(offset + 2, offset + 4), + radix: 16, + ); final name = _appletUsageNames[id]; if (name == null || !_hasAppletStorageUsage(id, functionSet)) { continue; } - usages.add(AppletStorageUsage( - id: id, - name: name, - logicalBytes: - int.parse(data.substring(offset + 4, offset + 12), radix: 16), - hasMissingSources: flags & 0x01 != 0, - )); + usages.add( + AppletStorageUsage( + id: id, + name: name, + logicalBytes: int.parse( + data.substring(offset + 4, offset + 12), + radix: 16, + ), + hasMissingSources: flags & 0x01 != 0, + ), + ); } return usages; } - Future _tryReadKeyboardKeymap() async { - var resp = await SmartCard.transceive('0046000001'); - if (SmartCard.sw(resp) == '6A88') { - return const KeyboardKeymapState( - layoutId: null, - entries: null, - preset: KeyboardKeymapPresets.defaultPreset, - isDefault: true, - ); - } - if (!SmartCard.isOK(resp)) { - log.w('Failed to read keyboard layout id: $resp'); - return const KeyboardKeymapState( - layoutId: null, - entries: null, - preset: null, - isDefault: false, - ); - } - final layoutId = int.parse(SmartCard.dropSW(resp), radix: 16); - - resp = await SmartCard.transceive('0046000100'); - if (!SmartCard.isOK(resp)) { - log.w('Failed to read keyboard keymap: $resp'); + Future _readKeyboardKeymap(String pin) async { + try { + final layoutId = await _client.readKeyboardLayout(pin: pin); + final entries = await _client.readKeyboardKeymap(pin: pin); return KeyboardKeymapState( layoutId: layoutId, - entries: null, - preset: KeyboardKeymapPresets.findById(layoutId), + entries: entries, + preset: KeyboardKeymapPresets.findMatching(layoutId, entries), isDefault: false, ); + } on ProtocolException catch (error) { + if (error.details.kind == 'NotFound') { + return const KeyboardKeymapState( + layoutId: null, + entries: null, + preset: KeyboardKeymapPresets.defaultPreset, + isDefault: true, + ); + } + rethrow; } - - final entries = Uint8List.fromList(hex.decode(SmartCard.dropSW(resp))); - final preset = KeyboardKeymapPresets.findMatching(layoutId, entries); - return KeyboardKeymapState( - layoutId: layoutId, - entries: entries, - preset: preset, - isDefault: false, - ); } static const int _appletUsageRecordLengthBytes = 6; static const int _appletUsageRecordLengthHex = _appletUsageRecordLengthBytes * 2; - static const int _appletUsageMinRecordCount = 7; - static const int _appletUsageMinResponseLengthHex = - _appletUsageRecordLengthHex * _appletUsageMinRecordCount; - static const Map _appletUsageNames = { 0x00: 'System', 0x01: 'Admin', @@ -388,32 +468,4 @@ class SettingsController extends PollingController with AdminApplet { return false; } } - - int _currentFeatureMask() => AppletSwitches.updateFeatureMask(0, { - Func.passSwitch: key.passEnabled, - Func.openPgpCcIdSwitch: key.openPgpCcIdEnabled, - Func.openPgpNfcSwitch: key.openPgpNfcEnabled, - Func.pivCcIdSwitch: key.pivCcIdEnabled, - Func.pivNfcSwitch: key.pivNfcEnabled, - Func.webAuthnSwitch: key.webAuthnEnabled, - }); - - String _changeFeatureSwitchesAPDU(Map values) { - final newMask = - AppletSwitches.updateFeatureMask(_currentFeatureMask(), values); - return '004006${newMask.toRadixString(16).padLeft(2, '0')}'; - } - - final Map> _changeSwitchAPDUs = { - Func.led: {true: '00400101', false: '00400100'}, - Func.hotp: {true: '00400301', false: '00400300'}, - Func.ndefEnabled: {true: '00400401', false: '00400400'}, - Func.ndefReadonly: {true: '00080100', false: '00080000'}, - Func.webusbLandingPage: {true: '00400501', false: '00400500'}, - Func.keyboardWithReturn: {true: '00400601', false: '00400600'}, - Func.sigTouch: {true: '00090001', false: '00090000'}, - Func.decTouch: {true: '00090101', false: '00090100'}, - Func.autTouch: {true: '00090201', false: '00090200'}, - Func.nfcSwitch: {true: '00140101', false: '00140100'}, - }; } diff --git a/lib/controller/applets/webauthn/webauthn_controller.dart b/lib/controller/applets/webauthn/webauthn_controller.dart index 954efa9..e8bc2ad 100644 --- a/lib/controller/applets/webauthn/webauthn_controller.dart +++ b/lib/controller/applets/webauthn/webauthn_controller.dart @@ -6,7 +6,6 @@ import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; import 'package:canokey_console/helper/theme/admin_theme.dart'; import 'package:canokey_console/helper/utils/applet_switches.dart'; -import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/ctap_transmitter.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; @@ -24,7 +23,8 @@ import 'package:logger/logger.dart'; class WebAuthnController extends PollingController with AdminApplet { late Ctap2 _ctap; - final Map _localPinCache = {}; + final CtapTransmitter _transmitter = CtapTransmitter(); + final _localPinCache = CredentialCache('webauthn'); final List webAuthnItems = []; FirmwareVersion firmwareVersion = const FirmwareVersion(0, 0, 0); FunctionSetVersion functionSetVersion = FunctionSetVersion.v1; @@ -100,8 +100,7 @@ class WebAuthnController extends PollingController with AdminApplet { return; } - SmartCard.assertOK( - await SmartCard.transceive('00A4040008A0000006472F0001')); + await _transmitter.selectApplication(); final cp = ClientPin(_ctap); try { await cp.changePin(pinToTry, newPin); @@ -135,7 +134,7 @@ class WebAuthnController extends PollingController with AdminApplet { if (!await authenticate(sn)) { return; } - config = await AdminCardClient().readSm2Config(); + config = await adminCardClient.readSm2Config(pin: adminPinForCurrentLease); }); return config; } @@ -152,7 +151,8 @@ class WebAuthnController extends PollingController with AdminApplet { if (!await authenticate(sn)) { return; } - await AdminCardClient().writeSm2Config( + await adminCardClient.writeSm2Config( + pin: adminPinForCurrentLease, enabled: enabled, curveId: curveId, algoId: algoId, @@ -174,8 +174,7 @@ class WebAuthnController extends PollingController with AdminApplet { return; } - SmartCard.assertOK( - await SmartCard.transceive('00A4040008A0000006472F0001')); + await _transmitter.selectApplication(); final cp = ClientPin(_ctap); final pinToken = await cp.getPinToken(pinToTry, permissions: [ClientPinPermission.credentialManagement]); @@ -212,9 +211,8 @@ class WebAuthnController extends PollingController with AdminApplet { } Future?> _getPinToken(String sn) async { - String resp = await SmartCard.transceive('00A4040008A0000006472F0001'); - SmartCard.assertOK(resp); - _ctap = await Ctap2.create(CtapTransmitter()); + await _transmitter.selectApplication(); + _ctap = await Ctap2.create(_transmitter); // We do nothing if the device does not support credMgmt or clientPin if (_ctap.info.options?['credMgmt'] != true || @@ -331,14 +329,12 @@ class WebAuthnController extends PollingController with AdminApplet { Prompts.stopPromptAndroidPolling(); try { // Set PIN and refresh by recreating Ctap2 - String resp = - await SmartCard.transceive('00A4040008A0000006472F0001'); - SmartCard.assertOK(resp); + await _transmitter.selectApplication(); final cp = ClientPin(_ctap); await cp.setPin(pin); // Update _ctap before continuing so later PIN-token operations see // the authenticator's new clientPin state. - _ctap = await Ctap2.create(CtapTransmitter()); + _ctap = await Ctap2.create(_transmitter); log.i('setPin success'); } on PlatformException catch (e) { await SmartCard.stopPollingNfc(withInput: true); @@ -374,8 +370,7 @@ class WebAuthnController extends PollingController with AdminApplet { Future?> _doGetPinToken(String pin) async { try { - String resp = await SmartCard.transceive('00A4040008A0000006472F0001'); - SmartCard.assertOK(resp); + await _transmitter.selectApplication(); final cp = ClientPin(_ctap); return await cp.getPinToken(pin, permissions: [ClientPinPermission.credentialManagement]); @@ -401,11 +396,10 @@ class WebAuthnController extends PollingController with AdminApplet { } Prompts.stopPromptAndroidPolling(); try { - SmartCard.assertOK( - await SmartCard.transceive('00A4040008A0000006472F0001')); + await _transmitter.selectApplication(); final cp = ClientPin(_ctap); await cp.changePin(currentPin, newPin); - _ctap = await Ctap2.create(CtapTransmitter()); + _ctap = await Ctap2.create(_transmitter); final pinToken = await _doGetPinToken(newPin); if (pinToken == null) { await SmartCard.stopPollingNfc(withInput: true); diff --git a/lib/controller/base/admin.dart b/lib/controller/base/admin.dart index d76e3bc..39c9938 100644 --- a/lib/controller/base/admin.dart +++ b/lib/controller/base/admin.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; @@ -20,9 +22,52 @@ final log = Logging.logger('AdminApplet'); /// re-prompting the user for PIN. /// If the user allows to save the PIN, the cache is also saved in the local storage, which /// is identified by the sn. -mixin AdminApplet { +mixin AdminApplet on GetxController { final AdminCardClient _adminCardClient = AdminCardClient(); - final Map _localPinCache = {}; + AdminCardClient get adminCardClient => _adminCardClient; + Uint8List? _leasePin; + CardLease? _pinLease; + String? _pinSerial; + int? _pinGeneration; + + /// Explicit input for each upstream Admin request, never an authorization token. + String get adminPinForCurrentLease { + final lease = SmartCard.currentLease; + if (!identical(lease, _pinLease) || _leasePin == null) { + throw StateError('Admin PIN was not verified in this lease'); + } + lease.check(); + if (_pinGeneration != + LocalStorage.credentialGeneration(_pinSerial!, _tag)) { + _forgetLeasePin(); + throw StateError('Admin credential was invalidated'); + } + return utf8.decode(_leasePin!); + } + + void _forgetLeasePin() { + _leasePin?.fillRange(0, _leasePin!.length, 0); + _leasePin = null; + _pinLease = null; + _pinSerial = null; + _pinGeneration = null; + } + + Future forgetAdminPin(String sn) async { + _forgetLeasePin(); + _localPinCache.remove(sn); + await LocalStorage.setPinCache(sn, _tag, null); + } + + @override + void onClose() { + adminCardClient.cancelPendingOperations(); + _forgetLeasePin(); + _localPinCache.clear(); + super.onClose(); + } + + final _localPinCache = CredentialCache('ADMIN'); final String _tag = 'ADMIN'; /// Returns true if CanoKey is authenticated. Must be called within SmartCard.process. @@ -30,9 +75,15 @@ mixin AdminApplet { /// We first try to use the local cache. If not cached, try LocalStorage. /// Finally, prompt the user for PIN. Future authenticate(String sn) async { + final attemptedPins = {}; + Future tryCachedPin(String pin) async { + if (!attemptedPins.add(pin)) return false; + return _selectAndVerifyPin(pin, sn); + } + // Try local cache first if (_localPinCache.containsKey(sn)) { - if (await _selectAndVerifyPin(_localPinCache[sn]!)) { + if (await tryCachedPin(_localPinCache[sn]!)) { return true; } _localPinCache.remove(sn); @@ -41,7 +92,7 @@ mixin AdminApplet { // Try LocalStorage String? pinToTry = LocalStorage.getPinCache(sn, _tag); if (pinToTry != null) { - if (await _selectAndVerifyPin(pinToTry)) { + if (await tryCachedPin(pinToTry)) { _localPinCache[sn] = pinToTry; return true; } else { @@ -64,25 +115,35 @@ mixin AdminApplet { if (!await SmartCard.pollNfcOrWebUsb()) { Prompts.stopPromptAndroidPolling(); Prompts.showPrompt( - S.of(Get.context!).noCard, ContentThemeColor.warning, - level: 'W'); + S.of(Get.context!).noCard, + ContentThemeColor.warning, + level: 'W', + ); Audio.error(); return; // timeout, do not close the dialog } Prompts.stopPromptAndroidPolling(); bool verified = false; try { - verified = await _selectAndVerifyPin(pin); + verified = await _selectAndVerifyPin(pin, sn); } on PlatformException catch (e) { await SmartCard.stopPollingNfc(withInput: true); log.e('_selectAndVerifyPin failed', error: e); if (e.code == '500') { Prompts.showPrompt( - S.of(Get.context!).interrupted, ContentThemeColor.danger); + S.of(Get.context!).interrupted, + ContentThemeColor.danger, + ); Audio.error(); } + } catch (error, stack) { + // Terminal protocol/lifecycle failures must settle the owning use case. + if (completer.isCompleted) return; + completer.completeError(error, stack); + Navigator.pop(Get.context!); + return; } - if (verified) { + if (verified && !completer.isCompleted) { log.t('PIN verified'); await updatePinCache(sn, pin, savePin); completer.complete(true); @@ -94,7 +155,7 @@ mixin AdminApplet { }, onCancel: () async { SmartCard.nfcState = NfcState.idle; - completer.complete(false); + if (!completer.isCompleted) completer.complete(false); }, ); return await completer.future; @@ -108,12 +169,26 @@ mixin AdminApplet { } /// Returns true if pin is verified - Future _selectAndVerifyPin(String pin) async { - await _adminCardClient.select(); - if (await _adminCardClient.verifyPin(pin)) { + Future _selectAndVerifyPin(String pin, String expectedSerial) async { + _forgetLeasePin(); + final lease = SmartCard.currentLease; + await adminCardClient.prepare(); + if (await adminCardClient.readSerial() != expectedSerial.toUpperCase()) { + throw StateError('Admin device changed before authentication'); + } + if (await adminCardClient.verifyPin(pin)) { + lease.check(); + _pinLease = lease; + _pinSerial = expectedSerial; + _pinGeneration = LocalStorage.credentialGeneration(expectedSerial, _tag); + final bytes = _leasePin = Uint8List.fromList(utf8.encode(pin)); + lease.onClose(() { + bytes.fillRange(0, bytes.length, 0); + if (identical(_leasePin, bytes)) _forgetLeasePin(); + }); return true; } else { - Prompts.promptPinFailureResult(_adminCardClient.lastResponse ?? ''); + Prompts.promptPinFailureResult(adminCardClient.lastResponse ?? ''); return false; } } diff --git a/lib/controller/base/polling_controller.dart b/lib/controller/base/polling_controller.dart index 52ab6f8..2a5c9b0 100644 --- a/lib/controller/base/polling_controller.dart +++ b/lib/controller/base/polling_controller.dart @@ -21,6 +21,9 @@ abstract class PollingController extends Controller { Future doRefreshData(); Logger get log; + /// Pages that own entry refreshes must not also refresh at controller creation. + bool get refreshWebOnReady => true; + @override void onReady() async { super.onReady(); @@ -39,10 +42,12 @@ abstract class PollingController extends Controller { if (isWeb()) { // Web platform: initial read and polling - try { - await refreshData(); - } catch (e) { - log.w('Failed to read card on web platform', error: e); + if (refreshWebOnReady) { + try { + await refreshData(); + } catch (e) { + log.w('Failed to read card on web platform', error: e); + } } if (_closed) return; _webPollTimer = Timer.periodic(const Duration(seconds: 1), (timer) { @@ -133,18 +138,23 @@ abstract class PollingController extends Controller { final timer = Stopwatch()..start(); log.t('refreshData: started'); late final Future refresh; - refresh = Future.sync(doRefreshData).then((_) { - _wasNfcConnection = SmartCard.connectionType == ConnectionType.nfc; - log.t("wasNfcConnection = $_wasNfcConnection"); - }, onError: (Object error, StackTrace stack) { - log.e('refreshData: failed', error: error, stackTrace: stack); - Error.throwWithStackTrace(error, stack); - }).whenComplete(() { - log.t('refreshData: finished in ${timer.elapsedMilliseconds}ms'); - if (identical(_refreshFuture, refresh)) { - _refreshFuture = null; - } - }); + refresh = Future.sync(doRefreshData) + .then( + (_) { + _wasNfcConnection = SmartCard.connectionType == ConnectionType.nfc; + log.t("wasNfcConnection = $_wasNfcConnection"); + }, + onError: (Object error, StackTrace stack) { + log.e('refreshData: failed', error: error, stackTrace: stack); + Error.throwWithStackTrace(error, stack); + }, + ) + .whenComplete(() { + log.t('refreshData: finished in ${timer.elapsedMilliseconds}ms'); + if (identical(_refreshFuture, refresh)) { + _refreshFuture = null; + } + }); _refreshFuture = refresh; return refresh; } diff --git a/lib/helper/storage/local_storage.dart b/lib/helper/storage/local_storage.dart index 6c37a29..8eb4dd1 100644 --- a/lib/helper/storage/local_storage.dart +++ b/lib/helper/storage/local_storage.dart @@ -47,9 +47,33 @@ class LocalStorage { return preferences.getInt(_nfcSoundKey); } + // Generations coordinate local credential caches without storing secrets here. + static final Map _credentialGenerations = {}; + static int _generation = 0; + static int credentialGeneration(String sn, String tag) => + _credentialGenerations.putIfAbsent('pin:$sn:$tag', () => _generation); + + static void _invalidateCredential(String key) { + _credentialGenerations[key] = ++_generation; + } + + /// Reset/change may have committed even when its acknowledgment is lost. + static Future clearPinCacheForDevice(String sn) async { + final prefix = 'pin:$sn:'; + final keys = { + ..._credentialGenerations.keys, + ...preferences.getKeys(), + }.where((key) => key.startsWith(prefix)).toList(); + for (final key in keys) { + _invalidateCredential(key); + await preferences.remove(key); + } + } + static Future setPinCache(String sn, String tag, String? pin) { log.t('Call LocalStorage.setPinCache'); if (pin == null) { + _invalidateCredential('pin:$sn:$tag'); return preferences.remove('pin:$sn:$tag'); } return preferences.setString('pin:$sn:$tag', pin); @@ -61,7 +85,13 @@ class LocalStorage { static Future clearPinCache() async { log.t('Call LocalStorage.clearPinCache'); - final keys = preferences.getKeys().where((key) => key.startsWith('pin:')); + final keys = { + ..._credentialGenerations.keys, + ...preferences.getKeys(), + }.where((key) => key.startsWith('pin:')).toList(); + for (final key in keys) { + _invalidateCredential(key); + } log.i('Clearing pin cache: $keys'); await Future.wait(keys.map((key) => preferences.remove(key))); } @@ -98,3 +128,29 @@ class LocalStorage { return preferences.getBool(_privacyAgreedKey) ?? false; } } + +/// A page-local credential cache invalidated by device PIN changes/resets on +/// another page. Values are inputs only and never assert live authentication. +class CredentialCache { + CredentialCache(this.tag); + final String tag; + final Map _entries = {}; + + String? operator [](String sn) { + final entry = _entries[sn]; + if (entry == null) return null; + if (entry.$2 != LocalStorage.credentialGeneration(sn, tag)) { + _entries.remove(sn); + return null; + } + return entry.$1; + } + + void operator []=(String sn, String value) { + _entries[sn] = (value, LocalStorage.credentialGeneration(sn, tag)); + } + + bool containsKey(String sn) => this[sn] != null; + void remove(String sn) => _entries.remove(sn); + void clear() => _entries.clear(); +} diff --git a/lib/helper/utils/admin_card.dart b/lib/helper/utils/admin_card.dart index 4d0e5a8..84f16c0 100644 --- a/lib/helper/utils/admin_card.dart +++ b/lib/helper/utils/admin_card.dart @@ -2,154 +2,440 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/webauthn.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; class AdminStorageUsage { const AdminStorageUsage({required this.usedKiB, required this.totalKiB}); - final int usedKiB; final int totalKiB; } +/// Each upstream Admin operation either SELECTs and explicitly authenticates +/// its own request, or reuses this lease's authenticated Admin session via +/// `Access::Existing`. That session evidence is recorded only by a successful +/// explicit [verifyPin] on the same lease and is bound to the lease's +/// selection/profile generations; a UI PIN cache is never authorization. class AdminCardClient { AdminCardClient({ ApduTransport transport = const SmartCardApduTransport(), - }) : _transport = transport; + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + final CardCancellation _cancellation = CardCancellation(); + _AdminProfileBinding? _profile; String? lastResponse; + AdminProgress? lastProgress; - Future select() async { - SmartCard.assertOK(await _transport.transceive('00A4040005F000000000')); + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected Admin transport requires withSession')); + + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); } - Future verifyPin(String pin) async { - final data = _encodePin(pin); - final response = lastResponse = await _transport.transceive( - '00200000${data.length.toRadixString(16).padLeft(2, '0')}' - '${hex.encode(data)}', + void cancelPendingOperations() => _cancellation.cancel(); + + /// Explicit minimal discovery, before any authentication. Repeat after a + /// profile-affecting write, including an uncertain write; never auto-reprobe. + Future prepare() async { + _cancellation.check(); + final lease = _lease; + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + ProtocolOperation.probeAdmin(), + _transport, + lease: lease, + cancellation: _cancellation, ); - return SmartCard.isOK(response); + final binding = _AdminProfileBinding(profile, lease); + try { + lease.check(); + _cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; + } } - Future changePin(String pin) async { - final data = _encodePin(pin); - SmartCard.assertOK(await _transport.transceive( - '00210000${data.length.toRadixString(16).padLeft(2, '0')}' - '${hex.encode(data)}', - )); + /// Explicit discovery only when the prepared binding is missing or stale. + /// A still-valid binding is reused as-is, preserving the lease's recorded + /// Admin session evidence; discovery is never repeated implicitly. + Future prepareIfStale() async { + final binding = _profile; + if (binding != null && identical(binding.lease, _lease)) { + try { + binding.check(); + _cancellation.check(); + return; + } on StateError { + // Fall through to explicit discovery below. + } + } + await prepare(); } - List _encodePin(String pin) { - final data = utf8.encode(pin); - if (data.length > 0xff) { - throw ArgumentError.value( - data.length, - 'encodedPinLength', - 'UTF-8 encoded PIN must fit in a short APDU', - ); + _AdminProfileBinding get _prepared { + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare Admin in the current lease first'); } - return data; + binding.check(); + _cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; } - Future readFirmwareVersion() => _readText('0031000000'); + /// Runs one Admin request. When this lease holds a still-valid recorded + /// Admin authentication (see CardLease.recordAdminAuthentication) and the + /// caller allows it, the request uses `Access::Existing`: no SELECT and no + /// VERIFY are sent, and any supplied PIN stays unused. Otherwise the + /// request SELECTs and verifies only the explicitly supplied PIN. + Future _execute( + ProtocolOperation Function(ProtocolProfile profile, Uint8List? pin, bool existing) + create, { + String? pin, + bool existingAllowed = true, + bool mutation = false, + }) async { + final binding = _prepared; + lastResponse = null; + lastProgress = null; + final existing = existingAllowed && binding.lease.hasAdminAuthentication; + if (!existing) { + // Selecting factories invalidate any selected PIV context before + // starting, even though immutable Admin profile evidence can be reused. + binding.lease.willSelectApplet(); + } + final pinBytes = existing || pin == null + ? null + : Uint8List.fromList(utf8.encode(pin)); + try { + final result = await executeAdminOperation( + create(binding.profile, pinBytes, existing), + _transport, + lease: binding.lease, + cancellation: _cancellation, + onProgress: (progress) => lastProgress = progress, + ); + binding.check(); + _cancellation.check(); + lastResponse = '9000'; + return result; + } on ProtocolException catch (error) { + lastResponse = error.details.statusWord + ?.toRadixString(16) + .padLeft(4, '0') + .toUpperCase(); + // A failed Existing request leaves the card-side session state + // uncertain; later requests must re-establish evidence or use a PIN. + if (existing) binding.lease.invalidateAdminAuthentication(); + rethrow; + } catch (_) { + binding.close(); + if (identical(_profile, binding)) _profile = null; + rethrow; + } finally { + pinBytes?.fillRange(0, pinBytes.length, 0); + final progress = lastProgress; + // An exposed or uncertain mutation ends the authenticated session + // evidence; no-op patches and pre-I/O validation failures retain it. + if (mutation && + progress != null && + (progress.confirmedWrites > 0 || progress.reprobeRequired)) { + binding.lease.invalidateAdminAuthentication(); + } + if (progress?.reprobeRequired == true) { + binding.lease.invalidateProfileEvidence(); + binding.close(); + if (identical(_profile, binding)) _profile = null; + } + } + } - Future readModel() => _readText('0031010000'); + Future _read(AdminReadOperation kind, {String? pin}) => _execute( + (profile, pinBytes, existing) => + profile.adminRead(kind: kind, pin: pinBytes, existing: existing), + pin: pin, + ); - Future readSm2Config() async { - final response = await _transport.transceive('0011000000'); - SmartCard.assertOK(response); - return WebAuthnSm2Config.decode( - hex.decode(SmartCard.dropSW(response)), - ); + Future _action( + AdminAction kind, { + String? pin, + List data = const [], + int index = 0, + int value = 0, + bool existingAllowed = true, + bool mutation = true, + }) async { + final payload = Uint8List.fromList(data); + try { + await _execute( + (profile, pinBytes, existing) => profile.adminAction( + kind: kind, + pin: pinBytes, + data: payload, + index: index, + value: value, + existing: existing, + ), + pin: pin, + existingAllowed: existingAllowed, + mutation: mutation, + ); + } finally { + payload.fillRange(0, payload.length, 0); + } } - Future writeSm2Config( - {required bool enabled, - required int curveId, - required int algoId}) async { - final current = await readSm2Config(); - final data = - current.encode(enabled: enabled, curveId: curveId, algoId: algoId); - SmartCard.assertOK(await _transport.transceive( - '00120000${data.length.toRadixString(16).padLeft(2, '0')}${hex.encode(data)}', - )); + /// The explicit Admin authentication step. Always SELECTs and verifies the + /// supplied PIN itself; only its success records this lease's authenticated + /// session evidence that later Existing requests rely on. + Future verifyPin(String pin) async { + try { + await _action( + AdminAction.verifyPin, + pin: pin, + existingAllowed: false, + mutation: false, + ); + // _prepared revalidates the binding and lease identity before stamping. + _prepared.lease.recordAdminAuthentication(); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked') { + return false; + } + rethrow; + } } - Future readSerial() => _readHex('0032000000'); + Future changePin(String pin, {required String currentPin}) async { + final bytes = Uint8List.fromList(utf8.encode(pin)); + try { + await _action(AdminAction.changePin, pin: currentPin, data: bytes); + } finally { + bytes.fillRange(0, bytes.length, 0); + } + } - Future readChipId() => _readHex('0032010000'); + Future readFirmwareVersion() async => utf8.decode( + _prepared.profile.firmware() ?? + (throw StateError('Missing firmware observation')), + ); + Future readModel() async => + _prepared.profile.model() ?? + (throw StateError('Device did not report its model')); + Future readSerial() async => hex + .encode( + _prepared.profile.serial() ?? + (throw StateError('Device did not report its serial')), + ) + .toUpperCase(); + Future readChipId() async => + hex.encode((await _read(AdminReadOperation.chipId)).data).toUpperCase(); - Future readConfig() async { - final response = await _transport.transceive('0042000000'); - SmartCard.assertOK(response); - return Uint8List.fromList(hex.decode(SmartCard.dropSW(response))); + Future readConfig({String? pin}) async => + (await _read(AdminReadOperation.configuration, pin: pin)).data; + Future readNfcEnabled({String? pin}) async => + (await _read(AdminReadOperation.nfcStatus, pin: pin)).data.single == 1; + Future readStorageUsage({String? pin}) async { + final data = (await _read(AdminReadOperation.flashUsage, pin: pin)).data; + return AdminStorageUsage(usedKiB: data[0], totalKiB: data[1]); } - Future writeConfigByte(int index, int value) async { - if (index < 0 || index > 0xff || value < 0 || value > 0xff) { - throw RangeError('Invalid admin configuration byte'); + Future readAppletStorageUsage() async => + (await _read(AdminReadOperation.appletUsage)).data; + + Future readKeyboardLayout({String? pin}) async => + (await _read(AdminReadOperation.keyboardLayout, pin: pin)).data.single; + + Future readKeyboardKeymap({String? pin}) async => + (await _read(AdminReadOperation.keyboardKeymap, pin: pin)).data; + + Future writeKeyboardKeymap( + int layoutId, + Uint8List keymap, { + required String pin, + }) async { + if (keymap.length != 256) { + throw ArgumentError.value(keymap.length, 'keymap'); } - final response = await _transport.transceive( - '0040${index.toRadixString(16).padLeft(2, '0')}' - '${value.toRadixString(16).padLeft(2, '0')}', + await _action( + AdminAction.keyboardKeymap, + pin: pin, + index: layoutId, + data: keymap, ); - SmartCard.assertOK(response); } - Future readNfcEnabled() async { - final response = await _transport.transceive('0014000000'); - SmartCard.assertOK(response); - final data = SmartCard.dropSW(response); - if (data.length != 2 || (data != '00' && data != '01')) { - throw const FormatException('Invalid NFC state'); + Future clearKeyboardKeymap({required String pin}) async { + await _action(AdminAction.clearKeyboardKeymap, pin: pin); + } + + Future readCoreCommit() async { + try { + final data = (await _read(AdminReadOperation.coreCommit)).data; + return data.isEmpty ? null : utf8.decode(data); + } on ProtocolException catch (error) { + if (error.details.kind == 'UnsupportedFeature') return null; + rethrow; } - return data == '01'; } - Future setNfcEnabled(bool enabled) async { - SmartCard.assertOK( - await _transport.transceive(enabled ? '00140101' : '00140100'), + Future configure({ + required String pin, + bool? ledOn, + bool? ndefReadOnly, + bool? ndefEnabled, + bool? webusbLanding, + int featureMask = 0, + int featureValues = 0, + }) async { + RangeError.checkValueInInterval(featureMask, 0, 0x3f, 'featureMask'); + RangeError.checkValueInInterval(featureValues, 0, 0x3f, 'featureValues'); + await _execute( + (profile, pinBytes, existing) => profile.adminConfigure( + pin: pinBytes, + existing: existing, + patch: AdminConfigurationPatch( + ledOn: ledOn, + ndefReadOnly: ndefReadOnly, + ndefEnabled: ndefEnabled, + webusbLanding: webusbLanding, + featureMask: featureMask, + featureValues: featureValues, + ), + ), + pin: pin, + mutation: true, ); } - Future readStorageUsage() async { - final response = await _transport.transceive('0041000002'); - SmartCard.assertOK(response); - final data = hex.decode(SmartCard.dropSW(response)); - if (data.length != 2) { - throw const FormatException('Invalid storage usage'); - } - return AdminStorageUsage(usedKiB: data[0], totalKiB: data[1]); + Future setKeyboardInterface(bool enabled, {required String pin}) => + _action(AdminAction.keyboardInterface, pin: pin, value: enabled ? 1 : 0); + Future setKeyboardReturn(bool enabled, {required String pin}) => + _action(AdminAction.keyboardReturn, pin: pin, value: enabled ? 1 : 0); + Future setLegacyPivExtensions(bool enabled, {required String pin}) => + _action( + AdminAction.legacyPivExtensions, + pin: pin, + value: enabled ? 1 : 0, + ); + Future setLegacyTouch(int index, int value, {required String pin}) { + RangeError.checkValueInInterval(index, 0, 3, 'index'); + RangeError.checkValueInInterval(value, 0, 0xff, 'value'); + return _action( + AdminAction.legacyTouch, + pin: pin, + index: index, + value: value, + ); } - Future readCoreCommit() async { - final response = await _transport.transceive('0031020000'); - if (!SmartCard.isOK(response)) return null; - final data = SmartCard.dropSW(response); - return data.isEmpty ? null : utf8.decode(hex.decode(data)); - } + Future setNfcEnabled(bool enabled, {required String pin}) => + _action(AdminAction.nfc, pin: pin, value: enabled ? 1 : 0); + Future setNdefReadOnly(bool readOnly, {required String pin}) => + configure(pin: pin, ndefReadOnly: readOnly); + Future resetNdef({required String pin}) => + resetApplet(Applet.ndef, pin: pin); + Future resetApplet(Applet applet, {required String pin}) => + _action(switch (applet) { + Applet.openpgp => AdminAction.resetOpenPgp, + Applet.piv => AdminAction.resetPiv, + Applet.oath => AdminAction.resetOath, + Applet.ndef => AdminAction.resetNdef, + Applet.webauthn => AdminAction.resetCtap, + Applet.pass => AdminAction.resetPass, + }, pin: pin); + Future factoryReset() => _action(AdminAction.factoryReset); - Future setNdefReadOnly(bool readOnly) async { - SmartCard.assertOK( - await _transport.transceive(readOnly ? '00080100' : '00080000'), + Future readSm2Config({required String pin}) async { + final result = await _read(AdminReadOperation.sm2Configuration, pin: pin); + // Legacy CanoKey 3.0.x copies packed native little-endian i32 fields. + // The variant comes from the profile, never from guessing response length. + return WebAuthnSm2Config.decode( + result.data, + legacyEndian: result.kind == AdminValueKind.legacySm2Configuration + ? Endian.little + : Endian.big, ); } - Future resetNdef() async { - SmartCard.assertOK(await _transport.transceive('00070000')); + Future writeSm2Config({ + required String pin, + required bool enabled, + required int curveId, + required int algoId, + }) async { + // Probe evidence chooses the format; this authenticated read validates it. + final current = await readSm2Config(pin: pin); + final data = current.encode( + enabled: enabled, + curveId: curveId, + algoId: algoId, + ); + await _action(AdminAction.writeSm2, pin: pin, data: data); } +} - Future _readText(String command) async { - final data = await _readHex(command); - return utf8.decode(hex.decode(data)); +class _AdminProfileBinding { + _AdminProfileBinding(this.profile, this.lease) + : generation = lease.profileGeneration; + final ProtocolProfile profile; + final CardLease lease; + final int generation; + bool _closed = false; + void check() { + lease.check(); + if (_closed || generation != lease.profileGeneration) { + throw StateError('Admin profile requires explicit discovery'); + } } - Future _readHex(String command) async { - final response = await _transport.transceive(command); - SmartCard.assertOK(response); - return SmartCard.dropSW(response).toUpperCase(); + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); + } } } diff --git a/lib/helper/utils/apdu_transport.dart b/lib/helper/utils/apdu_transport.dart index 13f0075..eafb396 100644 --- a/lib/helper/utils/apdu_transport.dart +++ b/lib/helper/utils/apdu_transport.dart @@ -11,8 +11,8 @@ class SmartCardApduTransport implements ApduTransport { Future transceive(String capdu) => SmartCard.transceive(capdu); } -/// ISO 7816 GET RESPONSE chaining. OATH uses applet-specific continuation -/// commands and deliberately keeps its own response loop. +/// ISO 7816 GET RESPONSE chaining. OATH operations go through libcanokey, +/// which owns its applet-specific continuation loop. extension ApduResponseChaining on ApduTransport { Future transceiveChained(String command) async { final data = StringBuffer(); diff --git a/lib/helper/utils/applet_switches.dart b/lib/helper/utils/applet_switches.dart index 92abc4c..5feb018 100644 --- a/lib/helper/utils/applet_switches.dart +++ b/lib/helper/utils/applet_switches.dart @@ -87,7 +87,7 @@ class AppletSwitches { AdminCardClient? client, }) async { final card = client ?? AdminCardClient(); - await card.select(); + await card.prepare(); final firmware = await card.readFirmwareVersion(); final functionSetVersion = CanoKey.functionSetFromFirmwareVersion(firmware); final config = functionSetVersion == FunctionSetVersion.v5 diff --git a/lib/helper/utils/card_session.dart b/lib/helper/utils/card_session.dart new file mode 100644 index 0000000..6a998a0 --- /dev/null +++ b/lib/helper/utils/card_session.dart @@ -0,0 +1,244 @@ +import 'dart:async'; + +/// A local cancellation request. Only the executor closes its Rust operation; +/// cancellation never races it to free a handle or abort an in-flight exchange. +class CardCancellation { + bool _cancelled = false; + void cancel() => _cancelled = true; + void check() { + if (_cancelled) throw StateError('Card operation cancelled'); + } +} + +/// Serializes whole use cases, including connection setup and cleanup. +/// The zone is only an ownership marker, never a global selected-app/profile cache. +class CardSessions { + final Object _zoneKey = Object(); + Future _tail = Future.value(); + int _generation = 0; + int _queued = 0; + + bool get isBusy => _queued != 0; + + CardSession? get current => Zone.current[_zoneKey] as CardSession?; + + void invalidate() => _generation++; + + Future run(Future Function(CardSession session) action) async { + if (current != null) { + throw StateError( + 'Nested card use cases must share their existing session', + ); + } + _queued++; + final previous = _tail; + final done = Completer(); + _tail = done.future; + await previous; + final session = CardSession._(this); + try { + return await runZoned( + () => action(session), + zoneValues: {_zoneKey: session}, + ); + } finally { + // Even an accidentally unawaited exchange must settle before reuse. + try { + await session._close(); + } finally { + _queued--; + done.complete(); + } + } + } +} + +class CardSession { + CardSession._(this._owner); + final CardSessions _owner; + CardLease? _lease; + bool _closed = false; + + CardLease get lease { + if (_closed) throw StateError('Card session is closed'); + final lease = _lease; + if (lease == null) throw StateError('Card connection has not been bound'); + lease.check(); + return lease; + } + + /// Rebinding is explicit after a new poll/connection. It clears all prior + /// selected-app evidence, and cannot replace an outstanding exchange. + void bind(Future Function(String) exchange) { + if (_closed || + (_lease?._pending != null) || + (_lease?._operationToken != null)) { + throw StateError('Cannot replace an active or closed card connection'); + } + _owner.invalidate(); + _lease?._closed = true; + _lease?._releaseResources(); + _lease = CardLease._(_owner, _owner._generation, exchange); + } + + Future _close() async { + _closed = true; + final lease = _lease; + if (lease != null) { + lease._closed = true; + await lease._pending; + lease._releaseResources(); + } + } +} + +/// A generation-bound raw channel. Keep this same object through start, every +/// advance and result publication. It owns neither APDU continuation nor retries. +class CardLease { + CardLease._(this._owner, this._generation, this._exchange); + final CardSessions _owner; + final int _generation; + final Future Function(String) _exchange; + bool _closed = false; + bool _failed = false; + Future? _pending; + Object? _operationToken; + + int _selectionGeneration = 0; + int _profileGeneration = 0; + int get selectionGeneration => _selectionGeneration; + int get profileGeneration => _profileGeneration; + + /// Call before a known selecting operation, never by inspecting APDU bytes. + void willSelectApplet() { + check(); + if (isExchanging) { + throw StateError('Cannot select during an active operation'); + } + _selectionGeneration++; + } + + /// Local invalidation is valid even after a lost response poisoned the lease. + /// Every applet's profile evidence must be refreshed after an exposed write. + void invalidateProfileEvidence() => _profileGeneration++; + + int? _adminAuthSelection; + int? _adminAuthProfile; + + /// Record an authenticated Admin session. Only an explicit, successful + /// facade VERIFY on this lease may call this; a UI credential cache never + /// creates this evidence. The stamp is bound to the current selection and + /// profile generations, so any later applet selection or profile + /// invalidation expires it without further bookkeeping. + void recordAdminAuthentication() { + check(); + _adminAuthSelection = _selectionGeneration; + _adminAuthProfile = _profileGeneration; + } + + /// Whether the recorded Admin authentication still matches both generations. + bool get hasAdminAuthentication => + _adminAuthSelection != null && + _adminAuthSelection == _selectionGeneration && + _adminAuthProfile == _profileGeneration; + + /// Explicit invalidation after an exposed or uncertain Admin mutation and + /// after a failed request that relied on the recorded session. + void invalidateAdminAuthentication() { + _adminAuthSelection = null; + _adminAuthProfile = null; + } + + /// Reserve the whole protocol operation, including gaps between exchanges. + /// Competing work fails before I/O rather than sharing selection/auth state. + CardOperationLease beginOperation() { + check(); + if (_pending != null || _operationToken != null) { + throw StateError('A card operation is already active'); + } + final token = _operationToken = Object(); + return CardOperationLease._(this, token); + } + + bool get isExchanging => _pending != null || _operationToken != null; + final List _resources = []; + + /// Transfer local resource cleanup to this physical lease, never a UI cache. + void onClose(void Function() release) { + check(); + _resources.add(release); + } + + void _releaseResources() { + Object? failure; + StackTrace? stack; + while (_resources.isNotEmpty) { + try { + _resources.removeLast()(); + } catch (error, trace) { + failure ??= error; + stack ??= trace; + } + } + if (failure != null) Error.throwWithStackTrace(failure, stack!); + } + + void check() { + if (_closed || _failed || _owner._generation != _generation) { + throw StateError('Card session is no longer current'); + } + } + + Future exchange(String command) => _exchangeOwned(command, null); + + Future _exchangeOwned(String command, Object? token) async { + check(); + if (!identical(token, _operationToken)) { + throw StateError('Raw exchange cannot interrupt a protocol operation'); + } + if (_pending != null) { + throw StateError('Concurrent exchange in a card session'); + } + final settled = Completer(); + _pending = settled.future; + try { + final response = await _exchange(command); + check(); + return response; + } catch (_) { + _failed = true; + rethrow; + } finally { + _pending = null; + settled.complete(); + } + } +} + +/// Executor-owned reservation. Releasing it is local and sends no APDU. +class CardOperationLease { + CardOperationLease._(this._lease, this._token); + final CardLease _lease; + final Object _token; + bool _closed = false; + + void check() { + _lease.check(); + if (_closed || !identical(_lease._operationToken, _token)) { + throw StateError('Card operation reservation is closed'); + } + } + + Future exchange(String command) { + check(); + return _lease._exchangeOwned(command, _token); + } + + void close() { + if (_closed) return; + _closed = true; + if (identical(_lease._operationToken, _token)) { + _lease._operationToken = null; + } + } +} diff --git a/lib/helper/utils/ctap_transmitter.dart b/lib/helper/utils/ctap_transmitter.dart index 2577ee5..8f27cf5 100644 --- a/lib/helper/utils/ctap_transmitter.dart +++ b/lib/helper/utils/ctap_transmitter.dart @@ -1,30 +1,72 @@ import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:fido2/fido2.dart'; -import 'package:convert/convert.dart'; +/// libcanokey-backed CTAP transport. The facade owns the APDU envelope +/// (extended Lc or CLA chaining) and GET RESPONSE continuation; this layer +/// only selects the FIDO2 applet once per lease and forwards raw CTAP +/// messages. A non-zero CTAP status byte is data for the fido2 package, +/// never a transport error. class CtapTransmitter extends CtapDevice { CtapTransmitter({ ApduTransport transport = const SmartCardApduTransport(), - }) : _transport = transport; + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; + final CardLease? _injectedLease; + + /// The card's selected applet is global to the physical lease, so the + /// selection evidence is shared by every transmitter on that lease. + static CardLease? _selectedLease; + bool _selectedWithoutLease = false; + + CardLease? get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease; + + bool get _isSelected { + final lease = _lease; + return lease != null + ? identical(_selectedLease, lease) + : _selectedWithoutLease; + } + + /// Explicit FIDO2 selection through libcanokey; an absent applet surfaces + /// as UnsupportedDevice instead of a raw 6A82 status word. + Future selectApplication() async { + final lease = _lease; + lease?.willSelectApplet(); + await executeProtocolOperation( + ProtocolOperation.ctapSelectApplication(), + _transport, + lease: lease, + ); + if (lease != null) { + _selectedLease = lease; + } else { + _selectedWithoutLease = true; + } + } @override Future>> transceive(List command) async { - final lc = command.length <= 255 - ? [command.length] - : [0, command.length >> 8, command.length & 0xff]; - var capdu = '80100000${hex.encode(lc)}${hex.encode(command)}'; - var rapdu = ''; - do { - if (rapdu.length >= 4) { - final remaining = rapdu.substring(rapdu.length - 2); - capdu = '80C00000$remaining'; - rapdu = rapdu.substring(0, rapdu.length - 4); - } - rapdu += await _transport.transceive(capdu); - } while (rapdu.substring(rapdu.length - 4, rapdu.length - 2) == '61'); - final response = hex.decode(rapdu); - return CtapResponse(response[0], response.sublist(1, response.length - 2)); + if (!_isSelected) { + await selectApplication(); + } + final data = await executeProtocolOperation( + ProtocolOperation.ctapTransceiveSelected(message: command), + _transport, + lease: _lease, + ); + return CtapResponse(data.first, data.sublist(1)); } } diff --git a/lib/helper/utils/ndef_card.dart b/lib/helper/utils/ndef_card.dart index 60b9341..9dddb1f 100644 --- a/lib/helper/utils/ndef_card.dart +++ b/lib/helper/utils/ndef_card.dart @@ -1,9 +1,10 @@ -import 'dart:math'; import 'dart:typed_data'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; -import 'package:convert/convert.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; class NdefCardData { const NdefCardData({ @@ -21,112 +22,90 @@ class NdefReadOnlyException implements Exception { const NdefReadOnlyException(); } +/// libcanokey-backed NDEF operations. These profile-free operations SELECT the +/// NDEF applet and own CC/file selection, 240-byte chunking, continuation and +/// the crash-safe write order (zero NLEN, message, real NLEN). A failed write +/// is never replayed; transport errors propagate unchanged. class NdefCardClient { - NdefCardClient({ApduTransport transport = const SmartCardApduTransport()}) - : _transport = transport; - - static const int _chunkSize = 240; - static const String _selectApplet = '00A4040007D2760000850101'; - static const String _selectCapabilityContainer = '00A4000C02E103'; - static const String _selectNdefFile = '00A4000C020001'; + NdefCardClient({ + ApduTransport transport = const SmartCardApduTransport(), + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; - - Future read() async { - if (!await _select()) return null; - - SmartCard.assertOK(await _transport.transceive(_selectCapabilityContainer)); - final capability = await _readBytes(0, 15); - if (capability.length != 15 || - capability[7] != 0x04 || - capability[8] != 0x06) { - throw const FormatException('Invalid NDEF capability container'); + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + final CardCancellation _cancellation = CardCancellation(); + + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected NDEF transport requires withSession')); + + /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); } - final fileLength = (capability[11] << 8) | capability[12]; - if (fileLength < 2) { - throw const FormatException('Invalid NDEF file capacity'); + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); } - - SmartCard.assertOK(await _transport.transceive(_selectNdefFile)); - final lengthData = await _readBytes(0, 2); - final messageLength = (lengthData[0] << 8) | lengthData[1]; - final maxMessageLength = fileLength - 2; - if (messageLength > maxMessageLength) { - throw FormatException( - 'NDEF message length $messageLength exceeds $maxMessageLength', - ); - } - - return NdefCardData( - maxMessageLength: maxMessageLength, - readOnly: capability[14] != 0, - message: await _readBytes(2, messageLength), - ); + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); } - Future write(Uint8List message) async { - if (!await _select()) return false; + /// Request cancellation; the active executor drains I/O and frees its handle. + void cancelPendingOperations() => _cancellation.cancel(); - SmartCard.assertOK(await _transport.transceive(_selectNdefFile)); - await _updateBytes(0, Uint8List.fromList([0, 0])); - for (var offset = 0; offset < message.length; offset += _chunkSize) { - final end = min(offset + _chunkSize, message.length); - await _updateBytes(2 + offset, message.sublist(offset, end)); + Future read() async { + final capability = await _execute(ProtocolOperation.ndefReadCapability()); + if (capability == null) return null; + if (capability.length != 3) { + throw const FormatException('Invalid NDEF capability data'); } - await _updateBytes( - 0, - Uint8List.fromList([message.length >> 8, message.length & 0xff]), + final message = await _execute(ProtocolOperation.ndefReadMessage()); + if (message == null) return null; + return NdefCardData( + maxMessageLength: (capability[0] << 8) | capability[1], + readOnly: capability[2] != 0, + message: message, ); - return true; - } - - Future _select() async { - final response = await _transport.transceive(_selectApplet); - if (SmartCard.sw(response) == '6A82') return false; - SmartCard.assertOK(response); - return true; } - Future _readBytes(int offset, int length) async { - final result = []; - for (var cursor = 0; cursor < length; cursor += _chunkSize) { - final count = min(_chunkSize, length - cursor); - final response = await _transport.transceive( - readBinaryApdu(offset + cursor, count), + Future write(Uint8List message) async => + await _execute(ProtocolOperation.ndefWriteMessage(message: message)) != + null; + + /// Null only when the NDEF applet is absent (6A82 at SELECT). + Future _execute(ProtocolOperation operation) async { + _cancellation.check(); + final lease = _lease; + // Every NDEF operation selects the applet before its target commands. + lease.willSelectApplet(); + try { + return await executeProtocolOperation( + operation, + _transport, + lease: lease, + cancellation: _cancellation, ); - SmartCard.assertOK(response); - result.addAll(hex.decode(SmartCard.dropSW(response))); - } - return Uint8List.fromList(result); - } - - Future _updateBytes(int offset, Uint8List data) async { - if (data.isEmpty) return; - final response = await _transport.transceive( - updateBinaryApdu(offset, data), - ); - if (SmartCard.sw(response) == '6982') { - throw const NdefReadOnlyException(); - } - SmartCard.assertOK(response); - } - - static String readBinaryApdu(int offset, int length) { - if (offset < 0 || offset > 0xffff || length < 1 || length > 0xff) { - throw RangeError('Invalid READ BINARY range'); - } - return '00B0${offset.toRadixString(16).padLeft(4, '0')}' - '${length.toRadixString(16).padLeft(2, '0')}' - .toUpperCase(); - } - - static String updateBinaryApdu(int offset, Uint8List data) { - if (offset < 0 || offset > 0xffff || data.isEmpty || data.length > 0xff) { - throw RangeError('Invalid UPDATE BINARY range'); + } on ProtocolException catch (error) { + if (error.details.kind == 'UnsupportedDevice' && + error.details.phase == 'Select') { + return null; + } + if (error.details.statusWord == 0x6982) { + throw const NdefReadOnlyException(); + } + rethrow; } - return '00D6${offset.toRadixString(16).padLeft(4, '0')}' - '${data.length.toRadixString(16).padLeft(2, '0')}' - '${hex.encode(data)}' - .toUpperCase(); } } diff --git a/lib/helper/utils/oath_card.dart b/lib/helper/utils/oath_card.dart index 15a84f3..5337692 100644 --- a/lib/helper/utils/oath_card.dart +++ b/lib/helper/utils/oath_card.dart @@ -1,56 +1,222 @@ import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; -import 'package:canokey_console/helper/tlv.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/oath.dart'; +import 'package:canokey_console/src/rust/api/crypto.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; +/// OATH applet observations from an explicit libcanokey SELECT. +/// A selection is evidence, never a reusable authenticated session. class OathSelection { const OathSelection({ - required this.response, required this.version, - required this.info, + this.salt, + required this.requiresCode, }); - final String response; final OathVersion version; - final Map info; + + /// Eight-byte key-derivation handle; absent on legacy firmware. + final Uint8List? salt; + + /// Whether an access code is installed and validation is required. + final bool requiresCode; +} + +/// One CalculateAll entry: either a truncated code or an explicit marker. +class OathCalculatedEntry { + const OathCalculatedEntry._({ + this.name, + required this.digits, + this.rawCode, + this.isHotp = false, + this.requiresTouch = false, + }); + + /// Absent only for an individual Calculate result. + final String? name; + final int digits; + + /// Big-endian dynamic-truncation value; null for marker entries. + final int? rawCode; + final bool isHotp; + final bool requiresTouch; } +/// libcanokey-backed OATH operations. The profile is explicitly prepared while +/// the caller owns the card lease; libcanokey then owns SELECT, framing, +/// pagination, continuation and response validation. Every operation selects +/// and, when an access key is supplied, validates within itself — no +/// authentication state is carried between operations. class OathCardClient { OathCardClient({ ApduTransport transport = const SmartCardApduTransport(), - }) : _transport = transport; + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; - OathVersion version = OathVersion.v1; + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + final CardCancellation _cancellation = CardCancellation(); + _OathProfileBinding? _profile; + String? lastStatusWord; - Future select() async { - final response = await _transport.transceive( - '00A4040007A0000005272101', + /// Fresh host challenge for each access proof; tests may pin a fixed value. + Uint8List Function() challengeGenerator = _randomChallenge; + + static Uint8List _randomChallenge() { + final random = Random.secure(); + return Uint8List.fromList(List.generate(8, (_) => random.nextInt(256))); + } + + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected OATH transport requires withSession')); + + /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); + } + + /// Request cancellation; the active executor drains I/O and frees its handle. + void cancelPendingOperations() => _cancellation.cancel(); + + /// OATH password convention: PBKDF2-HMAC-SHA1, 1000 iterations, the SELECT + /// handle as salt, sixteen output bytes. + static Uint8List deriveKey(String code, List salt) => + pbkdf2HmacSha1(password: code, salt: salt, iterations: 1000, keyLen: 16); + + /// Explicit minimal discovery, before any authentication. Repeat after a + /// profile-affecting write, including an uncertain write; never auto-reprobe. + Future prepare() async { + _cancellation.check(); + final lease = _lease; + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + ProtocolOperation.probeAdmin(), + _transport, + lease: lease, + cancellation: _cancellation, ); - SmartCard.assertOK(response); - if (response.toUpperCase() == '9000') { - version = OathVersion.legacy; - return OathSelection( - response: response, version: version, info: const {}); - } - - final info = TLV.parse(hex.decode(SmartCard.dropSW(response))); - final versionData = info[0x79]; - if (versionData is List) { - final encoded = hex.encode(versionData); - if (encoded == '050505') { - version = OathVersion.v1; - } else if (encoded == '060000') { - version = OathVersion.v2; - } + final binding = _OathProfileBinding(profile, lease); + try { + lease.check(); + _cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; } - return OathSelection(response: response, version: version, info: info); } - Future put({ + _OathProfileBinding get _prepared { + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare OATH in the current lease first'); + } + binding.check(); + _cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; + } + + Future _execute( + ProtocolOperation Function(ProtocolProfile) create, + ) async { + final binding = _prepared; + lastStatusWord = null; + // All OATH operations SELECT the applet before their target command. + binding.lease.willSelectApplet(); + try { + final data = await executeProtocolOperation( + create(binding.profile), + _transport, + lease: binding.lease, + cancellation: _cancellation, + ); + binding.check(); + _cancellation.check(); + lastStatusWord = '9000'; + return data; + } on ProtocolException catch (error) { + lastStatusWord = error.details.statusWord + ?.toRadixString(16) + .padLeft(4, '0') + .toUpperCase(); + rethrow; + } catch (_) { + binding.close(); + if (identical(_profile, binding)) _profile = null; + rethrow; + } + } + + (Uint8List?, Uint8List?) _accessParts(Uint8List? key) => + key == null ? (null, null) : (key, challengeGenerator()); + + Future select() async { + final data = await _execute((profile) => profile.oathSelect()); + // A legacy selection carries no fields, only the optional Admin serial. + if (data.length <= 4) { + return const OathSelection( + version: OathVersion.legacy, + requiresCode: false, + ); + } + if (data.length != 11 && data.length != 19) { + throw FormatException('Unexpected OATH selection of ${data.length} bytes'); + } + final version = switch (hex.encode(data.sublist(0, 3))) { + '050505' => OathVersion.v1, + '060000' => OathVersion.v2, + _ => OathVersion.v1, + }; + return OathSelection( + version: version, + salt: Uint8List.fromList(data.sublist(3, 11)), + requiresCode: data.length == 19, + ); + } + + /// Explicitly prove an access key. Wrong keys surface as AuthenticationFailed. + Future validate(Uint8List key) => + _execute((profile) => profile.oathValidate( + key: key, + challenge: challengeGenerator(), + )); + + Future put({ required String name, required String secretHex, required OathType type, @@ -58,34 +224,45 @@ class OathCardClient { required int digits, bool requireTouch = false, int initialValue = 0, + Uint8List? key, }) { if (initialValue < 0 || initialValue > 0xffffffff) { throw RangeError.range(initialValue, 0, 0xffffffff, 'initialValue'); } - final nameData = _nameData(name); - final credential = StringBuffer(nameData) - ..write('73') - ..write((secretHex.length ~/ 2 + 2).toRadixString(16).padLeft(2, '0')) - ..write((type.value | algorithm.value).toRadixString(16).padLeft(2, '0')) - ..write(digits.toRadixString(16).padLeft(2, '0')) - ..write(secretHex); - if (requireTouch) { - credential.write(version == OathVersion.legacy ? '780102' : '7802'); - } - if (initialValue > 0) { - credential.write( - '7A04${initialValue.toRadixString(16).padLeft(8, '0')}', - ); - } - return transceive(_command('00010000', credential.toString())); + final secret = Uint8List.fromList(hex.decode(secretHex)); + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathPut( + name: utf8.encode(name), + kind: type == OathType.hotp ? 1 : 2, + algorithm: algorithm.value, + digits: digits, + secret: secret, + requireTouch: requireTouch, + increasing: false, + initialCounter: initialValue, + accessKey: accessKey, + accessChallenge: accessChallenge, + )).whenComplete(() => secret.fillRange(0, secret.length, 0)); + } + + Future delete(String name, {Uint8List? key}) { + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathDelete( + name: utf8.encode(name), + accessKey: accessKey, + accessChallenge: accessChallenge, + )); } - Future calculate({ + /// Calculate one credential. The truncated code is returned unformatted; + /// display formatting (decimal, Steam) stays with the caller. + Future<(int digits, int rawCode)> calculate({ required String name, required OathType type, String? challengeHex, + Uint8List? key, }) { - final data = StringBuffer(_nameData(name)); + Uint8List? challenge; if (type == OathType.totp) { if (challengeHex == null || challengeHex.length != 16) { throw ArgumentError.value( @@ -94,13 +271,31 @@ class OathCardClient { 'TOTP challenges must contain eight bytes', ); } - data.write('7408$challengeHex'); + challenge = Uint8List.fromList(hex.decode(challengeHex)); } - final header = version == OathVersion.legacy ? '00040000' : '00A20001'; - return transceive(_command(header, data.toString())); + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathCalculate( + name: utf8.encode(name), + kind: type == OathType.hotp ? 1 : 2, + // Only full-format validation reads the expected algorithm. + algorithm: OathAlgorithm.sha1.value, + challenge: challenge, + format: 0, + accessKey: accessKey, + accessChallenge: accessChallenge, + )).then((data) { + final entry = _decodeCalculations(data).single; + if (entry.name != null || entry.rawCode == null) { + throw const FormatException('Unexpected OATH calculation result'); + } + return (entry.digits, entry.rawCode!); + }); } - Future calculateAll(String challengeHex) { + Future> calculateAll( + String challengeHex, { + Uint8List? key, + }) { if (challengeHex.length != 16) { throw ArgumentError.value( challengeHex, @@ -108,51 +303,132 @@ class OathCardClient { 'OATH challenges must contain eight bytes', ); } - final header = version == OathVersion.legacy ? '00050000' : '00A40001'; - return transceive(_command(header, '7408$challengeHex')); + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathCalculateAll( + challenge: Uint8List.fromList(hex.decode(challengeHex)), + format: 0, + accessKey: accessKey, + accessChallenge: accessChallenge, + )).then((data) => _decodeCalculations(data) + .map((entry) => entry.name != null + ? entry + : throw const FormatException('Nameless OATH list entry')) + .toList()); } - Future delete(String name) { - return transceive(_command('00020000', _nameData(name))); + /// Set or replace the access code. The current key validates in the same + /// operation; pass no old key only when the applet is unprotected. + Future setCode({required Uint8List newKey, Uint8List? oldKey}) => + _execute((profile) => profile.oathSetCode( + oldKey: oldKey, + newKey: newKey, + challenge: challengeGenerator(), + )); + + Future clearCode({Uint8List? key}) { + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathClearCode( + key: accessKey, + challenge: accessChallenge, + )); } - Future transceive(String capdu) async { - final isListCommand = - (version == OathVersion.legacy && capdu.startsWith('000500')) || - (version != OathVersion.legacy && capdu.startsWith('00A400')); - if (!isListCommand) { - return _transport.transceive(capdu); + /// Mark an existing HOTP credential as the touch keyboard-emulation + /// default: slot 0 = short, 1 = long. The legacy single-slot dialect comes + /// from the probed profile; a long slot or append-enter there fails + /// construction with InvalidArgument before any I/O. + Future setDefault({ + required String name, + required int slot, + required bool appendEnter, + Uint8List? key, + }) { + if (slot != 0 && slot != 1) { + throw RangeError.range(slot, 0, 1, 'slot'); } + final (accessKey, accessChallenge) = _accessParts(key); + return _execute((profile) => profile.oathSetDefault( + slot: slot, + appendEnter: appendEnter, + name: name, + accessKey: accessKey, + accessChallenge: accessChallenge, + )); + } - var response = await _transport.transceive(capdu); - final result = StringBuffer(); - while (true) { - final statusWord = SmartCard.sw(response); - final data = SmartCard.dropSW(response); - result.write(data); - if (statusWord == '9000' && data.isEmpty) { - return '${result}9000'; + // name_len (0xff = nameless) | name | digits | code tag | code_len | code. + static List _decodeCalculations(Uint8List data) { + final result = []; + var pos = 0; + int read() { + if (pos >= data.length) { + throw const FormatException('Truncated OATH calculation encoding'); } - if (statusWord.startsWith('61') || statusWord == '9000') { - final getResponse = - version == OathVersion.legacy ? '00060000FF' : '00A50000FF'; - response = await _transport.transceive(getResponse); - if (SmartCard.sw(response) == '6985') { - return '${result}9000'; - } - } else { - return '$result$statusWord'; + return data[pos++]; + } + + while (pos < data.length) { + final nameLength = read(); + if (nameLength != 0xff && (nameLength == 0 || pos + nameLength > data.length)) { + throw const FormatException('Truncated OATH calculation name'); + } + final name = nameLength == 0xff + ? null + : utf8.decode(data.sublist(pos, pos + nameLength)); + if (nameLength != 0xff) pos += nameLength; + final digits = read(); + final tag = read(); + final codeLength = read(); + if (pos + codeLength > data.length) { + throw const FormatException('Truncated OATH calculation code'); } + final code = data.sublist(pos, pos + codeLength); + pos += codeLength; + result.add(switch (tag) { + 0x76 when codeLength == 4 => OathCalculatedEntry._( + name: name, + digits: digits, + rawCode: ByteData.sublistView(code).getUint32(0), + ), + 0x77 when codeLength == 0 => OathCalculatedEntry._( + name: name, + digits: digits, + isHotp: true, + ), + 0x7c when codeLength == 0 => OathCalculatedEntry._( + name: name, + digits: digits, + requiresTouch: true, + ), + _ => throw const FormatException('Unexpected OATH calculation code'), + }); } + return result; } +} - String _nameData(String name) { - final bytes = utf8.encode(name); - return '71${bytes.length.toRadixString(16).padLeft(2, '0')}' - '${hex.encode(bytes)}'; +class _OathProfileBinding { + _OathProfileBinding(this.profile, this.lease) + : generation = lease.profileGeneration; + final ProtocolProfile profile; + final CardLease lease; + final int generation; + bool _closed = false; + + void check() { + lease.check(); + if (_closed || generation != lease.profileGeneration) { + throw StateError('OATH profile requires explicit discovery'); + } } - String _command(String header, String data) { - return '$header${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}$data'; + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); + } } } diff --git a/lib/helper/utils/openpgp_card.dart b/lib/helper/utils/openpgp_card.dart index 02ff733..8e9085f 100644 --- a/lib/helper/utils/openpgp_card.dart +++ b/lib/helper/utils/openpgp_card.dart @@ -1,25 +1,132 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:canokey_console/helper/tlv.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/openpgp.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; +/// libcanokey owns every OpenPGP operation: card-info reads, PW1/PW3 +/// credentials and all administrative writes. Each upstream operation SELECTs +/// OpenPGP and explicitly verifies its own password; the prepared profile is +/// immutable firmware evidence, never an authorization token. class OpenPgpCardClient { - OpenPgpCardClient({ApduTransport transport = const SmartCardApduTransport()}) - : _transport = transport; + OpenPgpCardClient({ + ApduTransport transport = const SmartCardApduTransport(), + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } - static const String _aid = 'D27600012401'; final ApduTransport _transport; + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + final CardCancellation _cancellation = CardCancellation(); + _OpenPgpProfileBinding? _profile; String? lastStatusWord; - Future select() async { - SmartCard.assertOK(await _transceive('00A4040006$_aid')); + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected OpenPGP transport requires withSession')); + + /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); + } + + void cancelPendingOperations() => _cancellation.cancel(); + + /// Explicit minimal discovery before any OpenPGP operation. The profile only + /// carries firmware observations; upstream operations SELECT OpenPGP themselves. + Future prepare() async { + _cancellation.check(); + final lease = _lease; + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + ProtocolOperation.probeAdmin(), + _transport, + lease: lease, + cancellation: _cancellation, + ); + final binding = _OpenPgpProfileBinding(profile, lease); + try { + lease.check(); + _cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; + } + } + + _OpenPgpProfileBinding get _prepared { + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare OpenPGP in the current lease first'); + } + binding.check(); + _cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; + } + + Future _execute( + ProtocolOperation Function(ProtocolProfile) create, + ) async { + final binding = _prepared; + lastStatusWord = null; + // Every upstream OpenPGP operation SELECTs its applet first. + binding.lease.willSelectApplet(); + try { + final data = await executeProtocolOperation( + create(binding.profile), + _transport, + lease: binding.lease, + cancellation: _cancellation, + ); + binding.check(); + _cancellation.check(); + lastStatusWord = '9000'; + return data; + } on ProtocolException catch (error) { + binding.check(); + _cancellation.check(); + lastStatusWord = error.details.statusWord + ?.toRadixString(16) + .padLeft(4, '0') + .toUpperCase(); + rethrow; + } } Future readCardInfo() async { - await select(); final applicationData = await _readDataObject(0x6E); final application = _parseApplicationRelatedData(applicationData); final discretionary = _parseDiscretionaryData(application); @@ -61,101 +168,209 @@ class OpenPgpCardClient { } Future changeUserPin(String oldPin, String newPin) => - _replacePin('00240081', oldPin, newPin); + _changePassword(0, oldPin, newPin); Future changeAdminPin(String oldPin, String newPin) => - _replacePin('00240083', oldPin, newPin); + _changePassword(2, oldPin, newPin); - Future _replacePin(String command, String secret, String newPin) async { - await select(); - final data = hex.encode([...utf8.encode(secret), ...utf8.encode(newPin)]); - return SmartCard.isOK(await _transceive(_capdu(command, data))); + /// PW1-sign (0) or PW3 (2); upstream sends old||new without a prior VERIFY. + Future _changePassword( + int reference, + String oldPin, + String newPin, + ) async { + final oldBytes = Uint8List.fromList(utf8.encode(oldPin)); + final newBytes = Uint8List.fromList(utf8.encode(newPin)); + try { + await _execute( + (profile) => profile.openpgpChangePassword( + reference: reference, + old: oldBytes, + new_: newBytes, + ), + ); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked') { + return false; + } + rethrow; + } finally { + oldBytes.fillRange(0, oldBytes.length, 0); + newBytes.fillRange(0, newBytes.length, 0); + } } - Future setResetCode(String adminPin, String resetCode) => - _runAdminOperation(adminPin, () { - final data = hex.encode(utf8.encode(resetCode)); - return _putDataObject(0xD3, data); - }); + Future verifyAdminPin(String adminPin) async { + final bytes = Uint8List.fromList(utf8.encode(adminPin)); + try { + await _execute( + (profile) => profile.openpgpVerify(reference: 2, password: bytes), + ); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked' || + error.details.kind == 'SecurityStatusNotSatisfied') { + return false; + } + rethrow; + } finally { + bytes.fillRange(0, bytes.length, 0); + } + } + /// Set (or clear with an empty string) the reset code via upstream + /// `DataWrite::ResetCode` after its own explicit PW3 verification. + Future setResetCode(String adminPin, String resetCode) async { + final code = resetCode.isEmpty + ? null + : Uint8List.fromList(utf8.encode(resetCode)); + try { + return await _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpWriteResetCode( + resetCode: code, + password: password, + ), + ); + } finally { + if (code != null) code.fillRange(0, code.length, 0); + } + } + + /// Upstream `Request::ResetRetries`, gated on the retry-reset capability. + /// On success the card resets PW1/PW3 to firmware defaults and clears + /// authorization; Console caches no OpenPGP credentials, so nothing local + /// needs invalidation. Never retried after an uncertain outcome. Future setPinRetries( String adminPin, int userRetries, int resetRetries, int adminRetries, - ) => _runAdminOperation(adminPin, () { - final data = hex.encode([userRetries, resetRetries, adminRetries]); - return _transceive(_capdu('00F20000', data)); - }); - + ) => _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpResetRetries( + retries: [userRetries, resetRetries, adminRetries], + password: password, + ), + ); + + /// Upstream `DataWrite::ReuseSignaturePin`; Console's flag is inverted. Future setSignaturePinPolicy( String adminPin, bool verifyForEverySignature, - ) => _runAdminOperation(adminPin, () { - final data = hex.encode([verifyForEverySignature ? 0x00 : 0x01]); - return _putDataObject(0xC4, data); - }); - - Future unblockUserPinWithAdmin(String adminPin, String newPin) => - _runAdminOperation(adminPin, () { - final data = hex.encode(utf8.encode(newPin)); - return _transceive(_capdu('002C0281', data)); - }); - - Future unblockUserPinWithResetCode(String resetCode, String newPin) => - _replacePin('002C0081', resetCode, newPin); + ) => _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpWriteSignaturePinPolicy( + reuse: !verifyForEverySignature, + password: password, + ), + ); + + /// Upstream `Request::UnblockWithAdmin` after explicit PW3 verification. + Future unblockUserPinWithAdmin(String adminPin, String newPin) async { + final newPinBytes = Uint8List.fromList(utf8.encode(newPin)); + try { + return await _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpUnblockWithAdmin( + newPin: newPinBytes, + password: password, + ), + ); + } finally { + newPinBytes.fillRange(0, newPinBytes.length, 0); + } + } - Future verifyAdminPin(String adminPin) async { - final data = hex.encode(utf8.encode(adminPin)); - return SmartCard.isOK(await _transceive(_capdu('00200083', data))); + /// Upstream `Request::UnblockWithCode`; no password verification is inserted. + Future unblockUserPinWithResetCode(String resetCode, String newPin) async { + final code = Uint8List.fromList(utf8.encode(resetCode)); + final newPinBytes = Uint8List.fromList(utf8.encode(newPin)); + try { + await _execute( + (profile) => + profile.openpgpUnblockWithCode(code: code, newPin: newPinBytes), + ); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked') { + return false; + } + rethrow; + } finally { + code.fillRange(0, code.length, 0); + newPinBytes.fillRange(0, newPinBytes.length, 0); + } } + /// Upstream `DataWrite::TouchPolicy`, emitting the standard two-byte UIF + /// field; the UIF capability gates construction on legacy firmware. Future setTouchPolicy( OpenPgpKeyType keyType, OpenPgpTouchPolicy policy, String adminPin, - ) => _runAdminOperation(adminPin, () { - final data = hex.encode([policy.value, 0x20]); - return _putDataObject(keyType.uifTag, data); - }); - - Future setTouchCacheTime(String adminPin, int seconds) => - _runAdminOperation(adminPin, () { - final data = hex.encode([seconds]); - return _putDataObject(0x0102, data); - }); + ) => _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpWriteTouchPolicy( + slot: keyType.index, + policy: policy.value, + password: password, + ), + ); + + /// Upstream `DataWrite::TouchCacheTime`; the UIF capability gates it. + Future setTouchCacheTime(String adminPin, int seconds) { + RangeError.checkValueInInterval(seconds, 0, 0xff, 'seconds'); + return _runAdminWrite( + adminPin, + (profile, password) => profile.openpgpWriteTouchCacheTime( + seconds: seconds, + password: password, + ), + ); + } - Future _runAdminOperation( + /// PW3-protected write. Credential rejection, blocked PW3 and target-stage + /// security rejections keep the UI's false result and status word; other + /// protocol and transport failures propagate. + Future _runAdminWrite( String adminPin, - Future Function() operation, + ProtocolOperation Function(ProtocolProfile profile, Uint8List password) + create, ) async { - await select(); - if (!await verifyAdminPin(adminPin)) return false; - return SmartCard.isOK(await operation()); + final password = Uint8List.fromList(utf8.encode(adminPin)); + try { + await _execute((profile) => create(profile, password)); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked' || + error.details.kind == 'SecurityStatusNotSatisfied') { + return false; + } + rethrow; + } finally { + password.fillRange(0, password.length, 0); + } } Future> _readDataObject(int tag) async { - final resp = await _getDataObject(tag); - SmartCard.assertOK(resp); - return hex.decode(SmartCard.dropSW(resp)); + return _execute((profile) => profile.openpgpReadData(tag: tag)); } + /// Optional objects: only a missing object (or an unsupported feature gate + /// before any exchange) becomes null; security/malformed failures propagate. Future?> _tryReadDataObject(int tag) async { - final resp = await _getDataObject(tag); - if (!SmartCard.isOK(resp)) { - return null; + try { + return await _readDataObject(tag); + } on ProtocolException catch (error) { + if (error.details.kind == 'NotFound') return null; + rethrow; } - return hex.decode(SmartCard.dropSW(resp)); - } - - Future _getDataObject(int tag) { - return _transceive('00CA${tag.toRadixString(16).padLeft(4, '0')}00'); - } - - Future _putDataObject(int tag, String data) { - return _transceive( - _capdu('00DA${tag.toRadixString(16).padLeft(4, '0')}', data), - ); } Map _parseUif(Map discretionary) { @@ -179,11 +394,18 @@ class OpenPgpCardClient { } Future _readTouchCacheTime() async { - final resp = await _transceive('00CA010201'); - if (!SmartCard.isOK(resp)) { - return null; + List data; + try { + data = await _readDataObject(0x0102); + } on ProtocolException catch (error) { + // Firmware before the UIF gate and cards without the object both keep + // the previous absent-cache-time behavior. + if (error.details.kind == 'NotFound' || + error.details.kind == 'UnsupportedFeature') { + return null; + } + rethrow; } - final data = hex.decode(SmartCard.dropSW(resp)); if (data.isEmpty) { return null; } @@ -316,21 +538,31 @@ class OpenPgpCardClient { (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3]; return DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true); } +} - String _capdu(String header, String data) { - return '$header${_hexLength(data.length ~/ 2)}$data'; - } - - String _hexLength(int length) { - if (length <= 0xFF) { - return length.toRadixString(16).padLeft(2, '0'); +/// Firmware-evidence binding; OpenPGP operations self-SELECT, so applet +/// switches do not invalidate it. Only profile evidence and lease matter. +class _OpenPgpProfileBinding { + _OpenPgpProfileBinding(this.profile, this.lease) + : generation = lease.profileGeneration; + final ProtocolProfile profile; + final CardLease lease; + final int generation; + bool _closed = false; + void check() { + lease.check(); + if (_closed || generation != lease.profileGeneration) { + throw StateError('OpenPGP profile requires explicit discovery'); } - return '00${length.toRadixString(16).padLeft(4, '0')}'; } - Future _transceive(String capdu) async { - final response = await _transport.transceiveChained(capdu); - lastStatusWord = SmartCard.sw(response); - return response; + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); + } } } diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index 6aa89d3..b644c3b 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -1,58 +1,276 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/pass.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; +/// Pass slot operations share the upstream Admin facade: each operation either +/// SELECTs Admin (a protected write verifying its own explicit PIN) or reuses +/// the lease's recorded Admin authentication via `Access::Existing`. A UI PIN +/// cache is never authorization evidence. class PassCardClient { PassCardClient({ ApduTransport transport = const SmartCardApduTransport(), - }) : _transport = transport; + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; - String? lastStatusWord; + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + final CardCancellation _cancellation = CardCancellation(); + _AdminProfileBinding? _profile; + String? lastResponse; + AdminProgress? lastProgress; + + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected Pass transport requires withSession')); + + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); + } + + void cancelPendingOperations() => _cancellation.cancel(); + + /// Explicit minimal Admin discovery, before any slot operation. Repeat after + /// a slot write, including an uncertain write; never auto-reprobe. + Future prepare() async { + _cancellation.check(); + final lease = _lease; + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + ProtocolOperation.probeAdmin(), + _transport, + lease: lease, + cancellation: _cancellation, + ); + final binding = _AdminProfileBinding(profile, lease); + try { + lease.check(); + _cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; + } + } + + _AdminProfileBinding get _prepared { + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare Admin in the current lease first'); + } + binding.check(); + _cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; + } + + /// Runs one Admin request. A still-valid recorded Admin authentication on + /// this lease selects `Access::Existing` (no SELECT/VERIFY, no PIN); any + /// other request SELECTs and verifies only its explicitly supplied PIN. + Future _execute( + ProtocolOperation Function(ProtocolProfile profile, Uint8List? pin, bool existing) + create, { + String? pin, + bool mutation = false, + }) async { + final binding = _prepared; + lastResponse = null; + lastProgress = null; + final existing = binding.lease.hasAdminAuthentication; + if (!existing) { + // Selecting factories invalidate any selected PIV context before + // starting, even though immutable Admin profile evidence can be reused. + binding.lease.willSelectApplet(); + } + final pinBytes = existing || pin == null + ? null + : Uint8List.fromList(utf8.encode(pin)); + try { + final result = await executeAdminOperation( + create(binding.profile, pinBytes, existing), + _transport, + lease: binding.lease, + cancellation: _cancellation, + onProgress: (progress) => lastProgress = progress, + ); + binding.check(); + _cancellation.check(); + lastResponse = '9000'; + return result; + } on ProtocolException catch (error) { + lastResponse = error.details.statusWord + ?.toRadixString(16) + .padLeft(4, '0') + .toUpperCase(); + // A failed Existing request leaves the card-side session uncertain. + if (existing) binding.lease.invalidateAdminAuthentication(); + rethrow; + } catch (_) { + binding.close(); + if (identical(_profile, binding)) _profile = null; + rethrow; + } finally { + pinBytes?.fillRange(0, pinBytes.length, 0); + final progress = lastProgress; + if (mutation && + progress != null && + (progress.confirmedWrites > 0 || progress.reprobeRequired)) { + binding.lease.invalidateAdminAuthentication(); + } + if (progress?.reprobeRequired == true) { + binding.lease.invalidateProfileEvidence(); + binding.close(); + if (identical(_profile, binding)) _profile = null; + } + } + } Future> readSlots() async { - final response = await _transport.transceive('0043000000'); - lastStatusWord = SmartCard.sw(response); - SmartCard.assertOK(response); - return PassSlot.fromData(SmartCard.dropSW(response)); + final result = await _execute( + (profile, pinBytes, existing) => + profile.adminPassSlots(pin: pinBytes, existing: existing), + ); + return PassSlot.decode(result.data); } + /// Returns false only when the card rejects or blocks the supplied PIN; + /// other protocol and transport failures propagate with their status word. Future setSlot( int index, PassSlotType type, String password, - bool withEnter, - ) async { + bool withEnter, { + required String pin, + }) async { if (index != 1 && index != 2) { throw ArgumentError.value(index, 'index', 'Pass slot must be 1 or 2'); } - final passwordBytes = password.codeUnits; - if (type == PassSlotType.static && - passwordBytes.any((byte) => byte < 0x20 || byte > 0x7E)) { - throw ArgumentError.value( - password, - 'password', - 'Static passwords must contain printable ASCII characters only', - ); - } - final data = switch (type) { - PassSlotType.none => '00', - PassSlotType.static => - '02${passwordBytes.length.toRadixString(16).padLeft(2, '0')}' - '${hex.encode(passwordBytes)}${withEnter ? '01' : '00'}', - PassSlotType.hmacSha1 => '0314$password', + final kind = switch (type) { + PassSlotType.none => 0, + PassSlotType.static => 1, + PassSlotType.hmacSha1 => 2, PassSlotType.oath => throw ArgumentError.value( - type, - 'type', - 'OATH slots are configured by the OATH applet', - ), + type, + 'type', + 'OATH slots are configured by the OATH applet', + ), + PassSlotType.unknown => throw ArgumentError.value( + type, + 'type', + 'Unknown Pass slots cannot be modified', + ), }; - final slot = index.toRadixString(16).padLeft(2, '0'); - final response = await _transport.transceive( - '0044${slot}00${(data.length ~/ 2).toRadixString(16).padLeft(2, '0')}$data', - ); - lastStatusWord = SmartCard.sw(response); - return SmartCard.isOK(response); + Uint8List? secret; + if (type == PassSlotType.static) { + final passwordBytes = password.codeUnits; + if (passwordBytes.length > 32 || + passwordBytes.any((byte) => byte < 0x20 || byte > 0x7E)) { + throw ArgumentError.value( + password, + 'password', + 'Static passwords must be at most 32 printable ASCII characters', + ); + } + secret = Uint8List.fromList(passwordBytes); + } else if (type == PassSlotType.hmacSha1) { + final List key; + try { + key = hex.decode(password); + } on FormatException { + throw ArgumentError.value( + password, + 'password', + 'HMAC-SHA1 keys are hex encoded', + ); + } + if (key.length != 20) { + throw ArgumentError.value( + password, + 'password', + 'HMAC-SHA1 keys are 20 bytes', + ); + } + secret = Uint8List.fromList(key); + } + try { + await _execute( + (profile, pinBytes, existing) => profile.adminSetPassSlot( + slot: index - 1, + kind: kind, + data: secret ?? const [], + appendEnter: type == PassSlotType.static && withEnter, + pin: pinBytes, + existing: existing, + ), + pin: pin, + mutation: true, + ); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked') { + return false; + } + rethrow; + } finally { + secret?.fillRange(0, secret.length, 0); + } + } +} + +class _AdminProfileBinding { + _AdminProfileBinding(this.profile, this.lease) + : generation = lease.profileGeneration; + final ProtocolProfile profile; + final CardLease lease; + final int generation; + bool _closed = false; + void check() { + lease.check(); + if (_closed || generation != lease.profileGeneration) { + throw StateError('Admin profile requires explicit discovery'); + } + } + + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); + } } } diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index 0a4e3b6..1bb7834 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'dart:typed_data'; -import 'package:canokey_console/helper/tlv.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; @@ -12,103 +12,508 @@ import 'package:convert/convert.dart'; class PivCardClient { PivCardClient({ ApduTransport transport = const SmartCardApduTransport(), + CardLease? lease, PivReadExecutor? readExecutor, + PivCertificateExecutor? certificateExecutor, + Future Function()? prepareExecutor, }) : _transport = transport, - _readExecutor = - readExecutor ?? - ((kind) => executeProtocolOperation( - ProtocolOperation.pivRead(kind: kind), - transport, - )); + _readExecutor = readExecutor, + _certificateExecutor = certificateExecutor, + _prepareExecutor = prepareExecutor, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } final ApduTransport _transport; - final PivReadExecutor _readExecutor; + final CardLease? _injectedLease; + final PivReadExecutor? _readExecutor; + final PivCertificateExecutor? _certificateExecutor; + final Future Function()? _prepareExecutor; + final CardSessions _injectedSessions = CardSessions(); + _PivProfileBinding? _profile; + + CardLease get _lease => _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? + _injectedSessions.current?.lease ?? + (throw StateError('Injected transport requires withSession')); + + /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); + } + + /// Explicit discovery/selection before authentication. The lease owns the + /// immutable observations; later metadata reads never probe or SELECT. + Future prepare() async { + _cancellation.check(); + final override = _prepareExecutor; + if (override != null) return override(); + final lease = _lease; + if (lease.isExchanging) { + throw StateError('Cannot replace a profile during an active operation'); + } + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + ProtocolOperation.probePiv(), + _transport, + lease: lease, + cancellation: _cancellation, + ); + final binding = _PivProfileBinding(profile, lease); + try { + lease.check(); + _cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; + } + } + + final CardCancellation _cancellation = CardCancellation(); + + /// Request cancellation; the active executor drains I/O and frees its handle. + void cancelPendingOperations() => _cancellation.cancel(); + + Future _read(PivReadOperation kind) async { + _cancellation.check(); + final binding = kind == PivReadOperation.select ? null : _profile; + binding?.check(); + final executor = _readExecutor; + if (executor != null) return executor(kind); + final data = await executeProtocolOperation( + ProtocolOperation.pivRead(kind: kind), + _transport, + lease: _transport is SmartCardApduTransport + ? null + : _injectedLease ?? _injectedSessions.current?.lease, + cancellation: _cancellation, + ); + binding?.check(); + _cancellation.check(); + return data; + } + String? lastStatusWord; Future select() async { - await _readExecutor(PivReadOperation.select); + final lease = _transport is SmartCardApduTransport + ? SmartCard.currentLease + : _injectedLease ?? _injectedSessions.current?.lease; + lease?.willSelectApplet(); + final binding = _profile; + if (binding != null) { + if (binding.lease.isExchanging) { + throw StateError('Cannot SELECT during an active operation'); + } + _discardProfile(binding); + } + await _read(PivReadOperation.select); } Future readVersion() async { - return _readExecutor(PivReadOperation.version); + return _read(PivReadOperation.version); + } + + /// Read the validated metadata directory without inserting SELECT. + Future readMetadataDirectory() async { + return _executePrepared((profile) => profile.pivMetadataDirectory()); + } + + Future generateKey({ + required int slot, + required int algorithm, + required int pinPolicy, + required int touchPolicy, + }) async { + return _executePrepared((profile) => profile.pivGenerateKey( + slot: slot, + algorithm: algorithm, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + )); + } + + Future deleteCertificate(int slot) async { + await _executePrepared((profile) => profile.pivDeleteCertificate(objectId: slot)); + } + + Future deleteKey(int slot) async { + await _executePrepared((profile) => profile.pivDeleteKey(slot: slot)); + } + + Future moveKey(int source, int target) async { + await _executePrepared((profile) => profile.pivMoveKey(source: source, target: target)); + } + + Future setManagementKey({ + required int algorithm, + required Uint8List key, + int touch = 0, + bool updateProtected = false, + }) async { + await _executePrepared((profile) => profile.pivSetManagementKey( + algorithm: algorithm, + key: key, + touch: touch, + updateProtected: updateProtected, + )); + } + + /// A successful write replaces the observed algorithm IDs; upstream marks it + /// ReprobeRequired, so the prepared profile is always discarded afterwards. + Future setAlgorithmConfig(Uint8List raw) async { + final binding = _prepared; + await _executePrepared((profile) => profile.pivSetAlgorithmConfig(raw: raw)); + _discardProfile(binding); + } + + Future setContainerName(int slot, String name) async { + await _executePrepared((profile) => profile.pivSetContainerName(slot: slot, name: name)); + } + + Future resetPinPukRetries(int pinRetries, int pukRetries) async { + await _executePrepared((profile) => profile.pivResetPinPukRetries( + pinRetries: pinRetries, + pukRetries: pukRetries, + )); + } + + Future sign({ + required int slot, + required int algorithm, + required Uint8List input, + int inputKind = 1, + }) async { + return _executePrepared((profile) => profile.pivSign( + slot: slot, + algorithm: algorithm, + input: input, + inputKind: inputKind, + )); + } + + /// Firmware streaming modes: 0 = ML-DSA-65 (empty context), 1 = randomized + /// Ed25519, 2 = SM2 full message. Classic digest/padded signing uses [sign]. + Future signStreaming({ + required int slot, + required int mode, + required Uint8List message, + Uint8List? userId, + }) async { + return _executePrepared((profile) => profile.pivSignStreaming( + slot: slot, + mode: mode, + message: message, + userId: userId, + )); + } + + /// INS F9 attestation certificate for a generated slot. + Future attest(int slot) async => + _executePrepared((profile) => profile.pivAttest(slot: slot)); + + Future decrypt(int slot, int algorithm, Uint8List ciphertext) async => + _executePrepared((profile) => profile.pivDecrypt( + slot: slot, + algorithm: algorithm, + ciphertext: ciphertext, + )); + + Future derive(int slot, int algorithm, Uint8List peer) async => + _executePrepared((profile) => profile.pivDerive( + slot: slot, + algorithm: algorithm, + peer: peer, + )); + + Future decapsulate(int slot, Uint8List ciphertext) async => + _executePrepared((profile) => profile.pivDecapsulate( + slot: slot, + ciphertext: ciphertext, + )); + + Future sm2Agreement({ + required int slot, + required int role, + required Uint8List peerStatic, + required Uint8List peerEphemeral, + Uint8List? userId, + Uint8List? peerId, + int keyLen = 32, + }) async => + _executePrepared((profile) => profile.pivSm2Agreement( + slot: slot, + role: role, + peerStatic: peerStatic, + peerEphemeral: peerEphemeral, + userId: userId, + peerId: peerId, + keyLen: keyLen, + )); + + Future importEcKey({ + required int slot, + required int algorithm, + required Uint8List scalar, + int pinPolicy = 0, + int touchPolicy = 0, + }) async { + await _executePrepared((profile) => profile.pivImportEcKey( + slot: slot, + algorithm: algorithm, + scalar: scalar, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + )); + } + + Future importRsaKey({ + required int slot, + required int algorithm, + required Uint8List p, + required Uint8List q, + required Uint8List dp, + required Uint8List dq, + required Uint8List qinv, + int pinPolicy = 0, + int touchPolicy = 0, + }) async { + await _executePrepared((profile) => profile.pivImportRsaKey( + slot: slot, + algorithm: algorithm, + p: p, + q: q, + dp: dp, + dq: dq, + qinv: qinv, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + )); + } + + Future importEd25519Key({ + required int slot, + required Uint8List seed, + int pinPolicy = 0, + int touchPolicy = 0, + }) async { + await _executePrepared((profile) => profile.pivImportEd25519Key( + slot: slot, + seed: seed, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + )); + } + + /// Import a post-quantum seed (INS FE): kind 0 = ML-DSA-65 (32-byte seed), + /// 1 = ML-KEM-768 (64-byte d||z seed). + Future importPqSeed({ + required int slot, + required int kind, + required Uint8List seed, + int pinPolicy = 0, + int touchPolicy = 0, + }) async { + await _executePrepared((profile) => profile.pivImportPqSeed( + slot: slot, + kind: kind, + seed: seed, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + )); + } + + _PivProfileBinding get _prepared { + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare PIV in this lease before this operation'); + } + binding.check(); + _cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; + } + + void _discardProfile(_PivProfileBinding binding) { + binding.close(); + if (identical(_profile, binding)) _profile = null; } + /// Use the serial observed before authentication; never switch applets here. Future readSerial() async { - final response = await _transport.transceive('00F8000000'); - SmartCard.assertOK(response); - return SmartCard.dropSW(response).toUpperCase(); + final serial = _prepared.profile.serial(); + if (serial == null) throw StateError('Device did not report a serial'); + return hex.encode(serial).toUpperCase(); } + Future _executePrepared( + ProtocolOperation Function(ProtocolProfile) create, { + bool allowMissing = false, + }) async { + final binding = _prepared; + lastStatusWord = null; + try { + final data = await executeProtocolOperation( + create(binding.profile), + _transport, + lease: binding.lease, + cancellation: _cancellation, + ); + binding.check(); + _cancellation.check(); + if (!identical(_profile, binding)) { + throw StateError('PIV profile replaced'); + } + lastStatusWord = '9000'; + return data; + } on ProtocolException catch (error) { + binding.check(); + _cancellation.check(); + final status = error.details.statusWord; + lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); + // A rejected credential leaves immutable device observations valid. An + // uncertain exchange/acknowledgment cannot be used for dependent work. + if (error.exchangeAttempted && + error.details.kind != 'AuthenticationFailed' && + error.details.kind != 'PinBlocked' && + !(allowMissing && error.details.kind == 'NotFound')) { + _discardProfile(binding); + } + rethrow; + } catch (_) { + _discardProfile(binding); + rethrow; + } + } + + /// Selected-only empty VERIFY; 9000 does not imply a known retry count. Future readPinRetries() async { - final response = await _transport.transceive('0020008000'); - lastStatusWord = SmartCard.sw(response); - return response; + final data = await _executePrepared( + (_) => ProtocolOperation.pivRead(kind: PivReadOperation.pinStatus), + ); + return lastStatusWord = hex.encode(data).toUpperCase(); } - /// Empty VERIFY queries remaining attempts without submitting a PIN. - /// 9000 means already authenticated, not a known retry count. Future readRemainingPinRetries() async { - final status = SmartCard.sw(await readPinRetries()).toUpperCase(); + final status = await readPinRetries(); if (status == '6983') return 0; - if (RegExp(r'^63C[0-9A-F]$').hasMatch(status)) { - return int.parse(status[3], radix: 16); - } + if (status.startsWith('63C')) return int.parse(status[3], radix: 16); return null; } - Future verifyPin(String pin) async { - final response = await _transport.transceive('0020008008${_padPin(pin)}'); - lastStatusWord = SmartCard.sw(response); - return SmartCard.isOK(response); - } + Future verifyPin(String pin) => + _credential(PivCredentialOperation.verifyPin, pin); Future changePin(String oldPin, String newPin) => - _changePin('00240080', oldPin, newPin); + _credential(PivCredentialOperation.changePin, oldPin, newPin); Future changePuk(String oldPuk, String newPuk) => - _changePin('00240081', oldPuk, newPuk); + _credential(PivCredentialOperation.changePuk, oldPuk, newPuk); Future unblockPin(String puk, String newPin) => - _changePin('002C0080', puk, newPin); + _credential(PivCredentialOperation.unblockPin, puk, newPin); - Future _changePin(String command, String oldPin, String newPin) async { - final response = await _transport.transceive( - '${command}10${_padPin(oldPin)}${_padPin(newPin)}', - ); - lastStatusWord = SmartCard.sw(response); - return SmartCard.isOK(response); + Future _credential( + PivCredentialOperation kind, + String current, [ + String replacement = '', + ]) async { + final currentBytes = Uint8List.fromList(current.codeUnits); + final replacementBytes = Uint8List.fromList(replacement.codeUnits); + try { + if (current.codeUnits.any((byte) => byte > 0xff) || + replacement.codeUnits.any((byte) => byte > 0xff)) { + throw ArgumentError( + 'PIV credentials must contain single-byte characters', + ); + } + await _executePrepared( + (profile) => profile.pivCredential( + kind: kind, + current: currentBytes, + replacement: replacementBytes, + ), + ); + return true; + } on ProtocolException catch (error) { + if (kind != PivCredentialOperation.logout && + (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'PinBlocked')) { + return false; + } + rethrow; + } finally { + currentBytes.fillRange(0, currentBytes.length, 0); + replacementBytes.fillRange(0, replacementBytes.length, 0); + } } - /// Exhaust PUK retries without changing or unblocking the PIN. - /// Only an explicit blocked response counts as success. + /// Explicit destructive workflow; each attempt is an upstream credential + /// operation. No retry follows transport, parsing or unexpected card failures. Future blockPuk() async { final random = Random.secure(); - // A byte-sized retry counter plus one final blocked-status probe. for (var attempt = 0; attempt < 257; attempt++) { final candidate = List.generate(8, (_) => random.nextInt(10)).join(); - final encoded = _padPin(candidate); - final response = await _transport.transceive( - '0024008110$encoded$encoded', - ); - final status = SmartCard.sw(response).toUpperCase(); - lastStatusWord = status; - if (status == '6983') return true; - // An accidental match leaves the PUK unchanged; try another candidate. - if (status != '9000' && !RegExp(r'^63C[0-9A-F]$').hasMatch(status)) { - return false; - } + await changePuk(candidate, candidate); + if (lastStatusWord == '6983') return true; } return false; } Future logout() async { - SmartCard.assertOK(await _transport.transceive('0020FF8000')); + await _credential(PivCredentialOperation.logout, ''); + } + + Future authenticateManagementKey( + String key, + AlgorithmType algorithm, + ) async { + final bytes = Uint8List.fromList(hex.decode(key)); + try { + await _executePrepared( + (profile) => profile.pivAuthenticateManagement( + algorithm: algorithm.value, + key: bytes, + ), + ); + return true; + } on ProtocolException catch (error) { + if (error.details.kind == 'AuthenticationFailed' || + error.details.kind == 'SecurityStatusNotSatisfied') { + return false; + } + rethrow; + } finally { + bytes.fillRange(0, bytes.length, 0); + } } Future readAlgorithmExtensions() async { await select(); try { - final data = await _readExecutor(PivReadOperation.algorithmConfiguration); + final data = await _read(PivReadOperation.algorithmConfiguration); return PivAlgorithmExtensionConfig.decode(data); } on ProtocolException catch (error) { // Only an unavailable instruction permits the existing firmware fallback. @@ -118,77 +523,154 @@ class PivCardClient { } } + /// Requires prepare() in this same lease. Algorithm interpretation comes from + /// its observed profile, never the caller's cached algorithmExtensionConfig. Future readMetadata( int slot, { PivAlgorithmExtensionConfig? algorithmExtensionConfig, }) async { - final response = await transceive( - '00F700${slot.toRadixString(16).padLeft(2, '0')}00', - ); - final statusWord = SmartCard.sw(response); - if (!SmartCard.isOK(response) || - statusWord == '6A88' || - statusWord == '6700') { - return null; + RangeError.checkValueInInterval(slot, 0, 0xff, 'slot'); + final lease = _lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare PIV in this lease before reading metadata'); } - final data = hex.decode(SmartCard.dropSW(response)); - if (algorithmExtensionConfig == null) { - return SlotInfo.parse(slot, data); + binding.check(); + _cancellation.check(); + lastStatusWord = null; + Uint8List data; + try { + data = await executeProtocolOperation( + binding.profile.pivMetadata(reference: slot), + _transport, + lease: lease, + cancellation: _cancellation, + ); + binding.check(); + _cancellation.check(); + lastStatusWord = '9000'; + } on ProtocolException catch (error) { + binding.check(); + _cancellation.check(); + final status = error.details.statusWord; + lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); + if (error.details.kind == 'NotFound') return null; + rethrow; } return SlotInfo.parse( slot, data, - algorithmExtensionConfig: algorithmExtensionConfig, + resolveAlgorithm: (wireId) { + if (slot == 0x80 || slot == 0x81 || slot == 0x9b) { + return AlgorithmType.fromValue(wireId); + } + final displayId = binding.profile.pivAlgorithmDisplayId(wireId: wireId); + if (displayId == null) { + throw FormatException('Unknown PIV metadata algorithm $wireId'); + } + return AlgorithmType.fromValue(displayId); + }, ); } - /// Read the DER bytes from a PIV certificate object (5FC1xx). - /// Tag 70 contains the certificate; tags 71 and FE are object metadata. + /// Read and decompress a PIV certificate through libcanokey without SELECT. + /// Only a missing object returns null; other card/protocol failures propagate. Future readCertificate(int objectId) async { - final response = await transceive( - '00CB3FFF055C035FC1${hex.encode([objectId])}00', - ); - if (!SmartCard.isOK(response)) return null; + RangeError.checkValueInInterval(objectId, 0, 0xff, 'objectId'); + final executor = _certificateExecutor; + final binding = executor == null ? _prepared : null; + lastStatusWord = null; try { - final object = TLV.parse(hex.decode(SmartCard.dropSW(response)))[0x53]; - if (object is! Uint8List) { - throw FormatException('Missing PIV certificate object (53)'); - } - final fields = TLV.parse(object); - final certificate = fields[0x70]; - if (certificate is! Uint8List || certificate.isEmpty) { - throw FormatException('Missing PIV certificate data (70)'); - } - final info = fields[0x71]; - if (info != null) { - if (info is! Uint8List || info.length != 1) { - throw FormatException('Invalid PIV certificate information (71)'); - } - if (info[0] & 1 != 0) { - throw FormatException( - 'Compressed PIV certificates are not supported', - ); - } - } + _cancellation.check(); + final certificate = executor != null + ? await executor(objectId) + : await executeProtocolOperation( + binding!.profile.pivCertificate(objectId: objectId), + _transport, + lease: binding.lease, + cancellation: _cancellation, + ); + _cancellation.check(); + binding?.check(); + lastStatusWord = '9000'; return certificate; - } on RangeError { - throw FormatException('Truncated PIV certificate object'); + } on ProtocolException catch (error) { + binding?.check(); + _cancellation.check(); + final status = error.details.statusWord; + lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); + if (error.details.phase == 'Command' && + (status == 0x6a82 || status == 0x6a88)) { + return null; + } + rethrow; } } - Future transceive(String capdu) async { - final response = await _transport.transceiveChained(capdu); - lastStatusWord = SmartCard.sw(response); - return response; + Uint8List _objectIdBytes(int objectId) { + RangeError.checkValueInInterval(objectId, 0, 0xffffff, 'objectId'); + return Uint8List.fromList( + hex.decode(objectId.toRadixString(16).padLeft(6, '0')), + ); } - String _padPin(String pin) { - final bytes = Uint8List(8)..fillRange(0, 8, 0xff); - final pinBytes = pin.codeUnits; - if (pinBytes.length > bytes.length) { - throw ArgumentError.value(pin, 'pin', 'PIV PIN must not exceed 8 bytes'); + /// Read the normalized 53 value; only an absent object becomes null. + Future readObject(int objectId) async { + final id = _objectIdBytes(objectId); + try { + return await _executePrepared( + (profile) => profile.pivReadObject(objectId: id), + allowMissing: true, + ); + } on ProtocolException catch (error) { + if (error.details.kind == 'NotFound') return null; + rethrow; + } + } + + /// The card checks existing authorization. Partial/uncertain writes terminate + /// preparation; the executor never replays a mutation or runs a rollback. + Future writeObject(int objectId, Uint8List data) async { + final id = _objectIdBytes(objectId); + final copy = Uint8List.fromList(data); + try { + await _executePrepared( + (profile) => profile.pivWriteObject(objectId: id, data: copy), + ); + } finally { + copy.fillRange(0, copy.length, 0); + } + } +} + +class _PivProfileBinding { + _PivProfileBinding(this.profile, this.lease) + : selectionGeneration = lease.selectionGeneration, + profileGeneration = lease.profileGeneration; + final ProtocolProfile profile; + final CardLease lease; + bool _closed = false; + final int selectionGeneration; + final int profileGeneration; + + void check() { + lease.check(); + if (_closed || + selectionGeneration != lease.selectionGeneration || + profileGeneration != lease.profileGeneration) { + throw StateError( + 'PIV selection or device observations are no longer current', + ); + } + } + + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); } - bytes.setRange(0, pinBytes.length, pinBytes); - return hex.encode(bytes).toUpperCase(); } } diff --git a/lib/helper/utils/piv_management_key.dart b/lib/helper/utils/piv_management_key.dart deleted file mode 100644 index a9780fb..0000000 --- a/lib/helper/utils/piv_management_key.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:typed_data'; - -import 'package:canokey_console/helper/tlv.dart'; -import 'package:canokey_console/models/piv.dart'; -import 'package:canokey_console/src/rust/api/crypto.dart'; -import 'package:convert/convert.dart'; - -class PivManagementKeyProtocol { - const PivManagementKeyProtocol._(); - - static int blockLength(AlgorithmType algorithm) => switch (algorithm) { - AlgorithmType.tdes => 8, - AlgorithmType.aes192 => 16, - _ => throw ArgumentError( - 'Unsupported management key algorithm: $algorithm'), - }; - - static String authenticateRequest(AlgorithmType algorithm) { - return '0087${_byteHex(algorithm.value)}9B047C028100'; - } - - static Uint8List parseChallenge(List response, AlgorithmType algorithm) { - final outer = TLV.parse(response); - final wrapped = outer[0x7C]; - if (wrapped is! List) { - throw FormatException('Invalid management key challenge'); - } - final inner = TLV.parse(wrapped); - final challenge = inner[0x81]; - if (challenge is! List || challenge.length != blockLength(algorithm)) { - throw FormatException('Invalid management key challenge length'); - } - return Uint8List.fromList(challenge); - } - - static Uint8List encryptChallenge({ - required AlgorithmType algorithm, - required List key, - required List challenge, - }) { - if (key.length != 24) { - throw ArgumentError('Management key must be 24 bytes'); - } - if (challenge.length != blockLength(algorithm)) { - throw ArgumentError('Invalid management key challenge length'); - } - return encryptPivManagementKeyChallenge( - algorithm: algorithm.value, - key: key, - challenge: challenge, - ); - } - - static String authenticateResponse( - AlgorithmType algorithm, List encryptedChallenge) { - if (encryptedChallenge.length != blockLength(algorithm)) { - throw ArgumentError('Invalid encrypted challenge length'); - } - final inner = - '82${_byteHex(encryptedChallenge.length)}${hex.encode(encryptedChallenge).toUpperCase()}'; - final data = '7C${_byteHex(inner.length ~/ 2)}$inner'; - return '0087${_byteHex(algorithm.value)}9B${_byteHex(data.length ~/ 2)}$data'; - } - - static String setManagementKey({ - required AlgorithmType algorithm, - required String key, - required TouchPolicy touchPolicy, - }) { - if (key.length != 48 || !RegExp(r'^[0-9a-fA-F]+$').hasMatch(key)) { - throw ArgumentError('Management key must be 24 bytes of hex'); - } - blockLength(algorithm); - final p2 = - algorithm == AlgorithmType.aes192 && touchPolicy == TouchPolicy.always - ? 'FE' - : 'FF'; - return '00FFFF${p2}1B${_byteHex(algorithm.value)}9B18${key.toUpperCase()}'; - } - - static String _byteHex(int value) => - value.toRadixString(16).padLeft(2, '0').toUpperCase(); -} diff --git a/lib/helper/utils/piv_post_quantum.dart b/lib/helper/utils/piv_post_quantum.dart deleted file mode 100644 index 1806c5a..0000000 --- a/lib/helper/utils/piv_post_quantum.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:typed_data'; - -import 'package:canokey_console/helper/tlv.dart'; -import 'package:canokey_console/models/piv.dart'; - -class PivPostQuantumProtocol { - static const int mlDsa65SeedLength = 32; - static const int mlDsa65PublicKeyLength = 1952; - static const int mlDsa65SignatureLength = 3309; - static const int mlKem768SeedLength = 64; - static const int mlKem768PublicKeyLength = 1184; - static const int mlKem768CiphertextLength = 1088; - static const int mlKem768SharedSecretLength = 32; - - static Uint8List buildImportData({ - required AlgorithmType algorithm, - required Uint8List seed, - required PinPolicy pinPolicy, - required TouchPolicy touchPolicy, - }) { - final (tag, expectedLength) = switch (algorithm) { - AlgorithmType.mldsa65 => (0x09, mlDsa65SeedLength), - AlgorithmType.mlkem768 => (0x0A, mlKem768SeedLength), - _ => - throw ArgumentError('Unsupported post-quantum algorithm: $algorithm'), - }; - if (seed.length != expectedLength) { - throw ArgumentError( - '${algorithm.label} seed must be $expectedLength bytes'); - } - return Uint8List.fromList([ - tag, - expectedLength, - ...seed, - 0xAA, - 0x01, - pinPolicy.value, - 0xAB, - 0x01, - touchPolicy.value, - ]); - } - - static Uint8List buildMlKemDecapsulationData(Uint8List ciphertext) { - if (ciphertext.length != mlKem768CiphertextLength) { - throw ArgumentError( - 'ML-KEM-768 ciphertext must be $mlKem768CiphertextLength bytes'); - } - return Uint8List.fromList([ - 0x7C, - 0x82, - 0x04, - 0x46, - 0x82, - 0x00, - 0x81, - 0x82, - 0x04, - 0x40, - ...ciphertext, - ]); - } - - static Uint8List parseMlKemSharedSecret(List response) { - final outer = TLV.parse(response); - final wrapped = outer[0x7C]; - if (wrapped is! List) { - throw ArgumentError('Invalid ML-KEM-768 response'); - } - final inner = TLV.parse(wrapped); - final secret = inner[0x82]; - if (secret is! List || secret.length != mlKem768SharedSecretLength) { - throw ArgumentError('Invalid ML-KEM-768 shared secret'); - } - return Uint8List.fromList(secret); - } -} diff --git a/lib/helper/utils/piv_signature.dart b/lib/helper/utils/piv_signature.dart index b538364..a761789 100644 --- a/lib/helper/utils/piv_signature.dart +++ b/lib/helper/utils/piv_signature.dart @@ -4,27 +4,6 @@ import 'package:canokey_console/helper/utils/piv_csr.dart'; import 'package:canokey_console/models/piv.dart'; import 'package:canokey_console/src/rust/api/crypto.dart'; -class PivSignatureProtocol { - static const int classicEd25519MaxMessageLength = 544; - static const int randomizedEd25519AlgorithmId = 0xFF; - - static int generalAuthenticateAlgorithmId({ - required AlgorithmType algorithm, - required int messageLength, - required PivAlgorithmExtensionConfig config, - }) { - if (algorithm == AlgorithmType.ed25519 && - messageLength > classicEd25519MaxMessageLength) { - if (!config.enabled) { - throw UnsupportedError( - 'Randomized Ed25519 signing requires algorithm extensions'); - } - return randomizedEd25519AlgorithmId; - } - return config.idFor(algorithm); - } -} - class PivSignatureTest { static PivPublicKey? publicKeyFromSlot(SlotInfo slot) { if (slot.public.isNotEmpty) { diff --git a/lib/helper/utils/protocol_operation.dart b/lib/helper/utils/protocol_operation.dart index cc63059..f59fd3d 100644 --- a/lib/helper/utils/protocol_operation.dart +++ b/lib/helper/utils/protocol_operation.dart @@ -1,15 +1,20 @@ import 'dart:typed_data'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; /// Protocol failures retain card status/context; transport exceptions pass through. class ProtocolException implements Exception { - const ProtocolException(this.details); + const ProtocolException(this.details, {this.exchangeAttempted = false}); final ProtocolError details; + /// Local executor evidence, independent of the protocol error phase. + final bool exchangeAttempted; + @override String toString() => 'libcanokey: ${details.kind} during ${details.phase}' @@ -21,22 +26,108 @@ class ProtocolException implements Exception { /// bounded response parsing, and safe Le correction. No implicit replay occurs. Future executeProtocolOperation( ProtocolOperation operation, - ApduTransport transport, -) async { + ApduTransport transport, { + CardLease? lease, + CardCancellation? cancellation, +}) => _execute( + operation, + transport, + lease: lease, + cancellation: cancellation, + result: (step) => step.data ?? (throw StateError('Expected byte result')), +); + +Future executeProfileProbe( + ProtocolOperation operation, + ApduTransport transport, { + required CardLease lease, + CardCancellation? cancellation, +}) => _execute( + operation, + transport, + lease: lease, + cancellation: cancellation, + result: (step) => + step.profile ?? (throw StateError('Expected profile result')), +); + +Future executeAdminOperation( + ProtocolOperation operation, + ApduTransport transport, { + required CardLease lease, + CardCancellation? cancellation, + required void Function(AdminProgress) onProgress, +}) => _execute( + operation, + transport, + lease: lease, + cancellation: cancellation, + onAdminProgress: onProgress, + result: (step) { + final result = step.admin ?? (throw StateError('Expected Admin result')); + onProgress(result.progress); + return result; + }, +); + +Future _execute( + ProtocolOperation operation, + ApduTransport transport, { + CardLease? lease, + CardCancellation? cancellation, + required T Function(ProtocolStep) result, + void Function(AdminProgress)? onAdminProgress, +}) async { + CardOperationLease? reservation; + var exchangeAttempted = false; try { + lease ??= transport is SmartCardApduTransport + ? SmartCard.currentLease + : null; + reservation = lease?.beginOperation(); + void check() { + reservation?.check(); + cancellation?.check(); + } + + check(); var step = operation.start(); while (true) { - if (step.error case final error?) throw ProtocolException(error); - if (step.data case final data?) return data; + if (step.error case final error?) { + throw ProtocolException(error, exchangeAttempted: exchangeAttempted); + } + if (step.data != null || step.profile != null || step.admin != null) { + try { + check(); + return result(step); + } catch (_) { + final data = step.data ?? step.admin?.data; + data?.fillRange(0, data.length, 0); + final profile = step.profile; + if (profile != null) { + try { + profile.close(); + } finally { + profile.dispose(); + } + } + rethrow; + } + } final command = step.command; if (command == null) throw StateError('Missing libcanokey command'); Uint8List? response; try { + check(); + final encoded = hex.encode(command).toUpperCase(); + exchangeAttempted = true; response = Uint8List.fromList( hex.decode( - await transport.transceive(hex.encode(command).toUpperCase()), + await (reservation?.exchange(encoded) ?? + transport.transceive(encoded)), ), ); + check(); step = operation.advance(response: response); } finally { command.fillRange(0, command.length, 0); @@ -44,9 +135,24 @@ Future executeProtocolOperation( } } } finally { - operation.close(); - operation.dispose(); + try { + try { + if (onAdminProgress != null) { + final progress = operation.adminProgress(); + if (progress != null) onAdminProgress(progress); + } + } finally { + operation.close(); + } + } finally { + try { + operation.dispose(); + } finally { + reservation?.close(); + } + } } } typedef PivReadExecutor = Future Function(PivReadOperation kind); +typedef PivCertificateExecutor = Future Function(int objectId); diff --git a/lib/helper/utils/smartcard.dart b/lib/helper/utils/smartcard.dart index 9efb4c5..ddbf2ea 100644 --- a/lib/helper/utils/smartcard.dart +++ b/lib/helper/utils/smartcard.dart @@ -1,12 +1,18 @@ import 'dart:async'; +import 'package:canokey_console/helper/utils/card_session.dart'; + import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/theme/admin_theme.dart'; +import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/audio.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:ccid/ccid.dart' if (dart.library.html) 'package:canokey_console/helper/ccid_dummy.dart'; +import 'package:convert/convert.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; @@ -59,9 +65,16 @@ enum NfcState { typedef RefreshCallback = Future Function(); class SmartCard { + static final CardSessions _sessions = CardSessions(); + + static CardLease get currentLease => + _sessions.current?.lease ?? + (throw StateError('Protocol operations require SmartCard.process')); + static String _currentSN = ''; static CcidCard? _ccidCard; + static bool _connectionQuarantined = false; static Completer? _androidNfcCompleter; @@ -162,9 +175,20 @@ class SmartCard { /// On iOS, the built-in keyboard will be hidden if an external keyboard is connected. /// This function shows the keyboard by sending an eject consumer report. static Future eject() async { - if (isIOSApp() && connectionType == ConnectionType.ccid) { - await _ccidCard?.transceive("FFEEFFEE"); + if (!isIOSApp() || connectionType != ConnectionType.ccid) return; + final session = _sessions.current; + if (session != null) { + // PIN prompts retain the owner zone. The keyboard consumer report must + // not overlap card I/O; it does not SELECT or change authentication. + final lease = session.lease; + if (!lease.isExchanging) await lease.exchange('FFEEFFEE'); + return; } + if (_sessions.isBusy) return; + await _sessions.run((session) async { + _bindConnection(); + await session.lease.exchange('FFEEFFEE'); + }); } static Future startAndroidNfcHandler() async { @@ -177,6 +201,7 @@ class SmartCard { readIso15693: false, androidPlatformSound: false, ); + if (connectionType != ConnectionType.ccid) _sessions.invalidate(); log.t('[nfcHandler] NFC tag polled: ${tag.id}'); switch (nfcState) { case NfcState.mute: @@ -192,29 +217,38 @@ class SmartCard { if (DateTime.now().millisecondsSinceEpoch - _lastFinishedTime < 2000) { log.t( - "[nfcHandler] Current state: $nfcState. Too soon. Ignored."); + "[nfcHandler] Current state: $nfcState. Too soon. Ignored.", + ); break; } log.t( - "[nfcHandler] Current state: $nfcState. Next state: refresh."); + "[nfcHandler] Current state: $nfcState. Next state: refresh.", + ); _beginAndroidNfcOperation(); Audio.poll(); Prompts.promptAndroidPolling(); nfcState = NfcState.refresh; - unawaited(handler().catchError((Object error, StackTrace stack) { - log.e('[nfcHandler] Failed to refresh NFC data.', - error: error, stackTrace: stack); - })); + unawaited( + handler().catchError((Object error, StackTrace stack) { + log.e( + '[nfcHandler] Failed to refresh NFC data.', + error: error, + stackTrace: stack, + ); + }), + ); case NfcState.refresh: log.e( - "[nfcHandler] Current state: $nfcState. No tag should be polled. Next state: idle."); + "[nfcHandler] Current state: $nfcState. No tag should be polled. Next state: idle.", + ); nfcState = NfcState.idle; case NfcState.processWithoutInput: case NfcState.processWithInput: log.t( - "[nfcHandler] Current state: $nfcState. Continue to process."); + "[nfcHandler] Current state: $nfcState. Continue to process.", + ); _androidNfcTimer?.cancel(); final completer = _androidNfcCompleter; if (completer != null && !completer.isCompleted) { @@ -231,14 +265,17 @@ class SmartCard { rethrow; } } catch (e) { - log.e('[nfcHandler] Current state: $nfcState. Error polling NFC tag.', - error: e); + log.e( + '[nfcHandler] Current state: $nfcState. Error polling NFC tag.', + error: e, + ); } await Future.delayed(const Duration(milliseconds: 100)); } } static Future pollNfcOrWebUsb() async { + _requireReusableConnection(); if (connectionType == ConnectionType.ccid) { // No need to poll return true; @@ -249,7 +286,8 @@ class SmartCard { case NfcState.idle: case NfcState.input: log.e( - "[pollNfcOrWebUsb] Tag should not be polled in $nfcState state."); + "[pollNfcOrWebUsb] Tag should not be polled in $nfcState state.", + ); return false; case NfcState.processWithoutInput: @@ -266,19 +304,25 @@ class SmartCard { completer.complete(false); if (nfcState == NfcState.processWithoutInput) { log.t( - "[pollNfcOrWebUsb] Current state: $nfcState. Timeout. Next state: idle."); + "[pollNfcOrWebUsb] Current state: $nfcState. Timeout. Next state: idle.", + ); nfcState = NfcState.idle; } else { log.t( - "[pollNfcOrWebUsb] Current state: $nfcState. Timeout. Next state: input."); + "[pollNfcOrWebUsb] Current state: $nfcState. Timeout. Next state: input.", + ); nfcState = NfcState.input; } }); - return completer.future; + final found = await completer.future; + if (found) _bindConnection(); + return found; case NfcState.refresh: log.t( - "[pollNfcOrWebUsb] Current state: refresh. Tag has been polled."); + "[pollNfcOrWebUsb] Current state: refresh. Tag has been polled.", + ); + _bindConnection(); return true; } } else { @@ -287,7 +331,9 @@ class SmartCard { await SystemChannels.textInput.invokeMethod('TextInput.hide'); } await FlutterNfcKit.poll( - iosAlertMessage: S.of(Get.context!).iosAlertMessage); + iosAlertMessage: S.of(Get.context!).iosAlertMessage, + ); + _bindConnection(); return true; } } @@ -303,8 +349,11 @@ class SmartCard { return ++_androidNfcOperation; } - static Future stopPollingNfc( - {bool withInput = false, int? operation, int? process}) async { + static Future stopPollingNfc({ + bool withInput = false, + int? operation, + int? process, + }) async { if (connectionType == ConnectionType.ccid) { return; } @@ -326,7 +375,8 @@ class SmartCard { switch (nfcState) { case NfcState.mute: log.e( - "[stopPollingNfc] Current state: $nfcState. Tag should not be polled."); + "[stopPollingNfc] Current state: $nfcState. Tag should not be polled.", + ); case NfcState.idle: log.t("[stopPollingNfc] Current state: $nfcState. Do nothing."); @@ -336,13 +386,15 @@ class SmartCard { case NfcState.processWithoutInput: final nextState = withInput ? NfcState.input : NfcState.idle; log.t( - "[stopPollingNfc] Current state: $nfcState. Next state: $nextState."); + "[stopPollingNfc] Current state: $nfcState. Next state: $nextState.", + ); nfcState = nextState; Audio.finish(); case NfcState.processWithInput: log.t( - "[stopPollingNfc] Current state: $nfcState. Next state: input."); + "[stopPollingNfc] Current state: $nfcState. Next state: input.", + ); nfcState = NfcState.input; case NfcState.input: // CHECKED CASE @@ -353,23 +405,30 @@ class SmartCard { case NfcState.refresh: // CHECKED CASE final nextState = withInput ? NfcState.input : NfcState.idle; log.t( - "[stopPollingNfc] Current state: $nfcState. Next state: $nextState."); + "[stopPollingNfc] Current state: $nfcState. Next state: $nextState.", + ); nfcState = nextState; _lastFinishedTime = DateTime.now().millisecondsSinceEpoch; Audio.finish(); } } else { + _sessions.invalidate(); await FlutterNfcKit.finish(); } } - static Future process(Function(String sn) f) async { + static Future process(Function(String sn) f) => + _sessions.run((_) => _process(f)); + + static Future _process(Function(String sn) f) async { + _requireReusableConnection(); final processGeneration = ++_cardProcessGeneration; final timer = Stopwatch()..start(); log.t('process #$processGeneration: started; connection=$connectionType'); _activeCardOperations++; try { if (connectionType == ConnectionType.ccid) { + _bindConnection(); await f(_currentSN); } else { if (nfcState == NfcState.idle) { @@ -383,24 +442,39 @@ class SmartCard { return; } try { - assertOK(await SmartCard.transceive('00A4040005F000000000')); - final resp = await SmartCard.transceive('0032000000'); - SmartCard.assertOK(resp); - final sn = SmartCard.dropSW(resp).toUpperCase(); + const transport = SmartCardApduTransport(); + await executeProtocolOperation( + ProtocolOperation.bootstrapIdentity( + step: BootstrapIdentityStep.selectAdmin, + ), + transport, + ); + final serial = await executeProtocolOperation( + ProtocolOperation.bootstrapIdentity( + step: BootstrapIdentityStep.serial, + ), + transport, + ); + final sn = hex.encode(serial).toUpperCase(); _currentSN = sn; if (isWeb()) { connectionType = ConnectionType.webusb; log.i( - '[process] CanoKey (WebUSB) Polled. SN: $sn. Connection Type updated to WebUSB.'); + '[process] CanoKey (WebUSB) Polled. SN: $sn. Connection Type updated to WebUSB.', + ); } else { connectionType = ConnectionType.nfc; log.i( - '[process] CanoKey (NFC) Polled. SN: $sn. Connection Type updated to NFC.'); + '[process] CanoKey (NFC) Polled. SN: $sn. Connection Type updated to NFC.', + ); } await f(sn); } on PlatformException catch (e, stack) { - log.e('process #$processGeneration: communication failed', - error: e, stackTrace: stack); + log.e( + 'process #$processGeneration: communication failed', + error: e, + stackTrace: stack, + ); if (e.message?.contains('SecurityError') == true) { // This is for WebUSB, handled by PollingController rethrow; @@ -409,37 +483,50 @@ class SmartCard { // TODO: check error messages if (e.message == 'NotFoundError: No device selected.') { Prompts.showPrompt( - S.of(Get.context!).pollCanceled, ContentThemeColor.danger); + S.of(Get.context!).pollCanceled, + ContentThemeColor.danger, + ); } else if (e.message == 'NetworkError: A transfer error has occurred.') { Prompts.showPrompt( - S.of(Get.context!).networkError, ContentThemeColor.danger); + S.of(Get.context!).networkError, + ContentThemeColor.danger, + ); } else if (e.message == 'SessionCanceled') { Prompts.showPrompt( - S.of(Get.context!).pollCanceled, ContentThemeColor.danger); + S.of(Get.context!).pollCanceled, + ContentThemeColor.danger, + ); } else if (e.code == '500') { Prompts.showPrompt( - S.of(Get.context!).interrupted, ContentThemeColor.danger); + S.of(Get.context!).interrupted, + ContentThemeColor.danger, + ); } else { Prompts.showPrompt( - S.current.operationFailed, ContentThemeColor.danger); + S.current.operationFailed, + ContentThemeColor.danger, + ); } if (isAndroidApp()) { Audio.error(); switch (nfcState) { case NfcState.refresh: log.t( - "[process] Current state: refresh. Communication error. Next state: idle."); + "[process] Current state: refresh. Communication error. Next state: idle.", + ); nfcState = NfcState.idle; case NfcState.processWithoutInput: log.t( - "[process] Current state: processWithoutInput. Communication error. Next state: idle."); + "[process] Current state: processWithoutInput. Communication error. Next state: idle.", + ); nfcState = NfcState.idle; case NfcState.processWithInput: log.t( - "[process] Current state: processWithInput. Communication error. Next state: input."); + "[process] Current state: processWithInput. Communication error. Next state: input.", + ); nfcState = NfcState.input; case NfcState.mute: @@ -450,37 +537,87 @@ class SmartCard { } } finally { await stopPollingNfc( - operation: _androidNfcOperation, process: processGeneration); + operation: _androidNfcOperation, + process: processGeneration, + ); } } } catch (error, stack) { - log.e('process #$processGeneration: failed', - error: error, stackTrace: stack); + log.e( + 'process #$processGeneration: failed', + error: error, + stackTrace: stack, + ); rethrow; } finally { log.t( - 'process #$processGeneration: finished in ${timer.elapsedMilliseconds}ms'); + 'process #$processGeneration: finished in ${timer.elapsedMilliseconds}ms', + ); _activeCardOperations--; } } - static Future transceive(String capdu) async { - String? rapdu; - log.d('C-APDU: $capdu'); - if (connectionType != ConnectionType.ccid) { - rapdu = await FlutterNfcKit.transceive(capdu); - } else { - if (_ccidCard == null) { - Prompts.showPrompt(S.of(Get.context!).noCard, ContentThemeColor.danger); - throw Exception('Card is not connected'); - } - rapdu = await _ccidCard!.transceive(capdu); - if (rapdu == null) { - throw Exception('Transceive failed'); - } + // Capture the physical connection once; never route an old operation through + // a newly selected global connection after an await. + static void _requireReusableConnection() { + if (_connectionQuarantined) { + throw StateError( + 'Card transport cleanup failed; restart the app before reuse', + ); } - log.d('R-APDU: $rapdu'); - return rapdu!; + } + + static void _bindConnection() { + _requireReusableConnection(); + final session = _sessions.current; + if (session == null) return; + final card = _ccidCard; + final ccid = connectionType == ConnectionType.ccid; + session.bind((command) async { + try { + log.d('C-APDU: $command'); + final response = ccid + ? await (card ?? (throw StateError('Card is not connected'))) + .transceive(command) + : await FlutterNfcKit.transceive(command); + if (response == null) throw StateError('Transceive failed'); + log.d('R-APDU: $response'); + return response; + } catch (_) { + // Isolate a failed/timed-out backend before the use-case lock is freed. + // Never replay the APDU. A later use case must connect/poll afresh. + _sessions.invalidate(); + _connectionQuarantined = true; + if (ccid) { + if (identical(_ccidCard, card)) _ccidCard = null; + _connectionQuarantined = !await _disconnectCcidCard(card); + } else { + try { + await FlutterNfcKit.finish(); + _connectionQuarantined = false; + } catch (_) { + // Preserve the original transport failure and prevent channel reuse. + } + } + if (_connectionQuarantined) { + connectionError = + 'Card transport cleanup failed; restart the app before reuse'; + } + _currentSN = ''; + connectionType = ConnectionType.none; + rethrow; + } + }); + } + + static Future transceive(String capdu) async { + final session = _sessions.current; + if (session != null) return session.lease.exchange(capdu); + // Legacy one-off users also respect an active protocol use-case lease. + return _sessions.run((session) async { + _bindConnection(); + return session.lease.exchange(capdu); + }); } static void pollCcid() { @@ -498,9 +635,11 @@ class SmartCard { } static Future _pollCcidOnce() async { + if (_connectionQuarantined) return; List readers; try { readers = await Ccid().listReaders(); + if (_connectionQuarantined) return; connectionError = null; _permissionDeniedCcidReaders.retainAll(readers); } catch (e) { @@ -509,10 +648,22 @@ class SmartCard { return; } + // Observe unplug events without waiting for an in-flight use case. Channel + // teardown and connection/probe APDUs still wait for exclusive ownership. + if (_ccidCard case final card?) { + if (!readers.contains(card.reader)) _sessions.invalidate(); + } + await _sessions.run((_) => _updateCcidConnection(readers)); + } + + static Future _updateCcidConnection(List readers) async { + if (_connectionQuarantined) return; final activeCard = _ccidCard; if (activeCard != null && !readers.contains(activeCard.reader)) { log.i( - 'CanoKey (USB) removed: $_currentSN. Connection Type updated to None.'); + 'CanoKey (USB) removed: $_currentSN. Connection Type updated to None.', + ); + _sessions.invalidate(); await _disconnectCcidCard(activeCard); _ccidCard = null; if (connectionType == ConnectionType.ccid) { @@ -524,9 +675,11 @@ class SmartCard { } } - final name = readers.firstWhereOrNull((name) => - name.toLowerCase().contains('canokey') && - !_permissionDeniedCcidReaders.contains(name)); + final name = readers.firstWhereOrNull( + (name) => + name.toLowerCase().contains('canokey') && + !_permissionDeniedCcidReaders.contains(name), + ); if (name == null || _ccidCard != null || _activeCardOperations > 0) { return; } @@ -534,13 +687,22 @@ class SmartCard { CcidCard? candidate; try { candidate = await Ccid().connect(name); - var resp = await candidate.transceive('00A4040005F000000000'); - assertOK(resp!); - resp = await candidate.transceive('0032000000'); - assertOK(resp!); + // The probe shares the use-case queue but binds no session yet; exchange + // directly on the candidate until it becomes the active card. + final probe = _CcidProbeTransport(candidate); + await executeProtocolOperation( + ProtocolOperation.bootstrapIdentity( + step: BootstrapIdentityStep.selectAdmin, + ), + probe, + ); + final serial = await executeProtocolOperation( + ProtocolOperation.bootstrapIdentity(step: BootstrapIdentityStep.serial), + probe, + ); _ccidCard = candidate; - _currentSN = SmartCard.dropSW(resp).toUpperCase(); + _currentSN = hex.encode(serial).toUpperCase(); connectionType = ConnectionType.ccid; if (isAndroidApp()) { _beginAndroidNfcOperation(); @@ -548,7 +710,8 @@ class SmartCard { nfcState = NfcState.mute; } log.i( - 'Successfully connected to CanoKey (USB). SN: $_currentSN. Connection Type updated to CCID.'); + 'Successfully connected to CanoKey (USB). SN: $_currentSN. Connection Type updated to CCID.', + ); } catch (e) { await _disconnectCcidCard(candidate); if (_isUsbPermissionDenied(e)) { @@ -562,14 +725,14 @@ class SmartCard { } } - static Future _disconnectCcidCard(CcidCard? card) async { - if (card == null) { - return; - } + static Future _disconnectCcidCard(CcidCard? card) async { + if (card == null) return true; try { await card.disconnect(); + return true; } catch (e) { log.w('Failed to disconnect CanoKey (USB)', error: e); + return false; } } @@ -590,9 +753,23 @@ class SmartCard { } static void onWebUSBDisconnected() { + _sessions.invalidate(); log.i( - 'CanoKey (WebUSB) removed: $_currentSN. Connection Type updated to None.'); + 'CanoKey (WebUSB) removed: $_currentSN. Connection Type updated to None.', + ); _currentSN = ''; connectionType = ConnectionType.none; } } + +/// Bootstrap probe of a not-yet-active CCID candidate. The caller still owns +/// connection cleanup and disconnects the candidate on any failure. +class _CcidProbeTransport implements ApduTransport { + const _CcidProbeTransport(this.card); + + final CcidCard card; + + @override + Future transceive(String capdu) async => + await card.transceive(capdu) ?? (throw StateError('Transceive failed')); +} diff --git a/lib/helper/widgets/input_pin_dialog.dart b/lib/helper/widgets/input_pin_dialog.dart index 8d88e65..7bed50c 100644 --- a/lib/helper/widgets/input_pin_dialog.dart +++ b/lib/helper/widgets/input_pin_dialog.dart @@ -22,6 +22,7 @@ class InputPinDialog extends BaseDialog { final List validators; final Future Function(String, bool) onSubmit; final Future Function() onCancel; + final Future Function() onFocus; const InputPinDialog({ super.key, @@ -33,6 +34,7 @@ class InputPinDialog extends BaseDialog { required this.validators, required this.onSubmit, required this.onCancel, + this.onFocus = SmartCard.eject, }); static void show({ @@ -53,8 +55,11 @@ class InputPinDialog extends BaseDialog { required: required, showSaveOption: showSaveOption, validators: validators, - onSubmit: onSubmit, - onCancel: onCancel, + // Flutter invokes UI callbacks in its own zone. Preserve the use-case + // lease while an authentication prompt is waiting for input. + onSubmit: Zone.current.bindBinaryCallback(onSubmit), + onCancel: Zone.current.bindCallback(onCancel), + onFocus: Zone.current.bindCallback(SmartCard.eject), ), ); } @@ -83,13 +88,21 @@ class _InputPinDialogState extends BaseDialogState { ); } + bool _submitting = false; + void _onSubmit() async { + if (_submitting) return; if (_validator.validateForm()) { - FocusManager.instance.primaryFocus?.unfocus(); - await widget.onSubmit( - _validator.getController('pin')!.text, - _savePin.value, - ); + _submitting = true; + try { + FocusManager.instance.primaryFocus?.unfocus(); + await widget.onSubmit( + _validator.getController('pin')!.text, + _savePin.value, + ); + } finally { + _submitting = false; + } } } @@ -123,7 +136,7 @@ class _InputPinDialogState extends BaseDialogState { autofocus: true, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - onTap: SmartCard.eject, + onTap: widget.onFocus, obscureText: !_showPin.value, controller: _validator.getController('pin'), validator: _validator.getValidator('pin'), diff --git a/lib/models/pass.dart b/lib/models/pass.dart index a3e4475..b806148 100644 --- a/lib/models/pass.dart +++ b/lib/models/pass.dart @@ -1,11 +1,6 @@ -import 'package:convert/convert.dart'; +import 'dart:typed_data'; -enum PassSlotType { - none, - oath, - static, - hmacSha1, -} +enum PassSlotType { none, oath, static, hmacSha1, unknown } class PassSlot { final PassSlotType type; @@ -18,47 +13,76 @@ class PassSlot { return PassSlot(type: PassSlotType.none, name: '', withEnter: false); } - // Parse slots from data, two in total. + // Decodes the bridge encoding, two slots in total: + // [short_len] ++ short ++ [long_len] ++ long. // For each slot, the first byte is the type. - // For PASS_SLOT_OFF, there is no more data - // For PASS_SLOT_STATIC, the second byte is with_enter + // For PASS_SLOT_OFF, there is no more data. + // For PASS_SLOT_STATIC, the second byte is with_enter. // For PASS_SLOT_HMACSHA1, there is no more data because the key is never dumped back. // For PASS_SLOT_OATH, the next byte is the length of the name, followed by the name, and the next byte is with_enter. - static List fromData(String hexData) { - List slots = []; - int i = 0; - while (i < hexData.length) { - int type = int.parse(hexData.substring(i, i + 2), radix: 16); - i += 2; - if (type == 0) { - // OFF - slots - .add(PassSlot(type: PassSlotType.none, name: '', withEnter: false)); - } else if (type == 1) { - // OATH - int nameLen = int.parse(hexData.substring(i, i + 2), radix: 16); - i += 2; - String nameHex = hexData.substring(i, i + nameLen * 2); - String name = String.fromCharCodes(hex.decode(nameHex)); - i += nameLen * 2; - int withEnter = int.parse(hexData.substring(i, i + 2), radix: 16); - i += 2; - slots.add(PassSlot( - type: PassSlotType.oath, name: name, withEnter: withEnter == 1)); - } else if (type == 2) { - // STATIC - int withEnter = int.parse(hexData.substring(i, i + 2), radix: 16); - i += 2; - slots.add(PassSlot( - type: PassSlotType.static, name: '', withEnter: withEnter == 1)); - } else if (type == 3) { - // HMAC-SHA1 - slots.add( - PassSlot(type: PassSlotType.hmacSha1, name: '', withEnter: false)); - } else { - throw FormatException('Unsupported Pass slot type: $type'); + // Any other type byte is an unknown state carried verbatim. + static List decode(Uint8List data) { + var offset = 0; + List nextState() { + if (offset >= data.length) { + throw const FormatException('Missing Pass slot state'); + } + final length = data[offset++]; + if (offset + length > data.length) { + throw const FormatException('Truncated Pass slot state'); } + final state = data.sublist(offset, offset + length); + offset += length; + return state; + } + + final slots = [_decodeState(nextState()), _decodeState(nextState())]; + if (offset != data.length) { + throw const FormatException('Trailing Pass slot data'); } return slots; } + + static PassSlot _decodeState(List state) { + if (state.isEmpty) { + throw const FormatException('Empty Pass slot state'); + } + switch (state[0]) { + case 0x00: + if (state.length != 1) { + throw const FormatException('Malformed empty Pass slot'); + } + return PassSlot(type: PassSlotType.none, name: '', withEnter: false); + case 0x02: + if (state.length != 2) { + throw const FormatException('Malformed static Pass slot'); + } + return PassSlot( + type: PassSlotType.static, + name: '', + withEnter: state[1] == 1, + ); + case 0x03: + if (state.length != 1) { + throw const FormatException('Malformed HMAC-SHA1 Pass slot'); + } + return PassSlot( + type: PassSlotType.hmacSha1, + name: '', + withEnter: false, + ); + case 0x01: + final nameLength = state.length > 1 ? state[1] : -1; + if (state.length != nameLength + 3) { + throw const FormatException('Malformed OATH Pass slot'); + } + return PassSlot( + type: PassSlotType.oath, + name: String.fromCharCodes(state.sublist(2, 2 + nameLength)), + withEnter: state[2 + nameLength] == 1, + ); + default: + return PassSlot(type: PassSlotType.unknown, name: '', withEnter: false); + } + } } diff --git a/lib/models/piv.dart b/lib/models/piv.dart index 53027dc..f367127 100644 --- a/lib/models/piv.dart +++ b/lib/models/piv.dart @@ -338,37 +338,53 @@ class SlotInfo { static SlotInfo parse( int number, List buf, { + AlgorithmType Function(int wireId)? resolveAlgorithm, PivAlgorithmExtensionConfig algorithmExtensionConfig = PivAlgorithmExtensionConfig.defaults, }) { Map map = TLV.parse(buf); - final algoId = map[0x01][0] as int; - var algo = algorithmExtensionConfig.enabled + List requiredField(int tag, int length) { + final value = map[tag]; + if (value is! List || value.length != length) { + throw FormatException( + 'PIV metadata lacks a displayable field ${tag.toRadixString(16)}', + ); + } + return value; + } + final algoId = requiredField(0x01, 1)[0]; + var algo = resolveAlgorithm != null + ? resolveAlgorithm(algoId) + : algorithmExtensionConfig.enabled ? (algorithmExtensionConfig.toAlgorithmMap()[algoId] ?? AlgorithmType.fromValue(algoId)) : AlgorithmType.fromValue(algoId); var pinPolicy = PinPolicy.defaultPolicy; var touchPolicy = TouchPolicy.defaultPolicy; if (number != 0x80 && number != 0x81) { - pinPolicy = PinPolicy.fromValue(map[0x02][0]); - touchPolicy = TouchPolicy.fromValue(map[0x02][1]); + pinPolicy = PinPolicy.fromValue(requiredField(0x02, 2)[0]); + touchPolicy = TouchPolicy.fromValue(requiredField(0x02, 2)[1]); } var origin = Origin.generated; if (number != 0x80 && number != 0x81 && number != 0x9B) { - origin = Origin.fromValue(map[0x03][0]); + origin = Origin.fromValue(requiredField(0x03, 1)[0]); } List public = []; if (number != 0x80 && number != 0x81 && number != 0x9B) { - public = map[0x04]; + public = (map[0x04] as List?) ?? const []; } var defaultValue = false; if (number == 0x80 || number == 0x81 || number == 0x9B) { - defaultValue = map[0x05][0] == 0x01; + final flag = requiredField(0x05, 1)[0]; + if (flag != 0 && flag != 1) { + throw const FormatException('Unknown PIV default-credential flag'); + } + defaultValue = flag == 1; } var retriesCount = 0, remainingCount = 0; if (number == 0x80 || number == 0x81) { - retriesCount = map[0x06][0]; - remainingCount = map[0x06][1]; + retriesCount = requiredField(0x06, 2)[0]; + remainingCount = requiredField(0x06, 2)[1]; } return SlotInfo(number, algo, pinPolicy, touchPolicy, origin, public, defaultValue, retriesCount, remainingCount); diff --git a/lib/models/webauthn.dart b/lib/models/webauthn.dart index 082f750..8b1145b 100644 --- a/lib/models/webauthn.dart +++ b/lib/models/webauthn.dart @@ -7,15 +7,20 @@ class WebAuthnSm2Config { final int curveId; final int algoId; final bool canChangeEnabled; + final Endian legacyEndian; const WebAuthnSm2Config({ required this.enabled, required this.curveId, required this.algoId, this.canChangeEnabled = true, + this.legacyEndian = Endian.big, }); - factory WebAuthnSm2Config.decode(List bytes) { + factory WebAuthnSm2Config.decode( + List bytes, { + Endian legacyEndian = Endian.big, + }) { if (bytes.length != 8 && bytes.length != 9) { throw const FormatException('Invalid WebAuthn SM2 configuration length'); } @@ -27,9 +32,10 @@ class WebAuthnSm2Config { final offset = legacy ? 1 : 0; return WebAuthnSm2Config( enabled: !legacy || bytes[0] == 1, - curveId: data.getInt32(offset, Endian.big), - algoId: data.getInt32(offset + 4, Endian.big), + curveId: data.getInt32(offset, legacy ? legacyEndian : Endian.big), + algoId: data.getInt32(offset + 4, legacy ? legacyEndian : Endian.big), canChangeEnabled: legacy, + legacyEndian: legacyEndian, ); } @@ -56,8 +62,16 @@ class WebAuthnSm2Config { final data = ByteData(canChangeEnabled ? 9 : 8); final offset = canChangeEnabled ? 1 : 0; if (canChangeEnabled) data.setUint8(0, enabled ? 1 : 0); - data.setInt32(offset, curveId, Endian.big); - data.setInt32(offset + 4, algoId, Endian.big); + data.setInt32( + offset, + curveId, + canChangeEnabled ? legacyEndian : Endian.big, + ); + data.setInt32( + offset + 4, + algoId, + canChangeEnabled ? legacyEndian : Endian.big, + ); return data.buffer.asUint8List(); } } diff --git a/lib/src/rust/api/protocol.dart b/lib/src/rust/api/protocol.dart index 2c125cc..fec9cc9 100644 --- a/lib/src/rust/api/protocol.dart +++ b/lib/src/rust/api/protocol.dart @@ -6,30 +6,641 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `drive`, `drive`, `failure` +// These functions are ignored because they are not marked as `pub`: `admin_operation`, `admin_progress`, `admin_result`, `bytes`, `drive`, `drive`, `failure`, `from_operation`, `oath_access`, `openpgp_slot`, `pass_slots_data`, `piv_slot` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Inner` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `from` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `from` // Rust type: RustOpaqueMoi> abstract class ProtocolOperation implements RustOpaqueInterface { + /// Retained upstream progress, available even after protocol failure. + AdminProgress? adminProgress(); + /// Consume one complete, unprocessed response including SW1/SW2. /// The input is copied into zeroizing storage and released before returning. ProtocolStep advance({required List response}); + /// One raw bootstrap identity step for connection setup. The caller's + /// exclusive lease spans SELECT and the serial read; a final non-9000 + /// status fails with the raw status word retained. No retry or replay. + static ProtocolOperation bootstrapIdentity({ + required BootstrapIdentityStep step, + }) => RustLib.instance.api.crateApiProtocolProtocolOperationBootstrapIdentity( + step: step, + ); + /// Release locally, including on a transport exception. Never retry/rollback. void close(); + /// Select the FIDO2 applet only; the caller owns the selected context. + static ProtocolOperation ctapSelectApplication() => RustLib.instance.api + .crateApiProtocolProtocolOperationCtapSelectApplication(); + + /// Select the FIDO2 applet and send one CTAP message within that selection. + static ProtocolOperation ctapTransceive({required List message}) => + RustLib.instance.api.crateApiProtocolProtocolOperationCtapTransceive( + message: message, + ); + + /// One CTAP message to the caller's already selected FIDO2 applet. + /// Data is the CTAP status byte followed by the payload; a non-success + /// status is not a protocol error and stays visible to Dart. + static ProtocolOperation ctapTransceiveSelected({ + required List message, + }) => RustLib.instance.api + .crateApiProtocolProtocolOperationCtapTransceiveSelected( + message: message, + ); + + /// Profile-free NDEF capability-container read. Selects the NDEF applet. + /// Data encodes `max_message_length` (big-endian u16, excluding the two + /// NLEN bytes) followed by the read-only flag byte. + static ProtocolOperation ndefReadCapability() => RustLib.instance.api + .crateApiProtocolProtocolOperationNdefReadCapability(); + + /// Profile-free full NDEF message read; chunked by the upstream machine. + /// Data is the raw message without the two NLEN bytes. + static ProtocolOperation ndefReadMessage() => + RustLib.instance.api.crateApiProtocolProtocolOperationNdefReadMessage(); + + /// Crash-safe NDEF message replacement: zero NLEN, message chunks, real + /// NLEN. A failed mid-write leaves no stale message and is never replayed. + static ProtocolOperation ndefWriteMessage({required List message}) => + RustLib.instance.api.crateApiProtocolProtocolOperationNdefWriteMessage( + message: message, + ); + /// Construct without I/O. Only Select changes applet selection; reads never /// probe, SELECT, authenticate, or retain a device profile implicitly. static ProtocolOperation pivRead({required PivReadOperation kind}) => RustLib.instance.api.crateApiProtocolProtocolOperationPivRead(kind: kind); + /// Admin-only discovery before authentication; does not select PIV. + static ProtocolOperation probeAdmin() => + RustLib.instance.api.crateApiProtocolProtocolOperationProbeAdmin(); + + /// Explicit Admin/PIV discovery. Call before authentication in the same + /// exclusive lease; the completed profile is transferred to Dart exactly once. + static ProtocolOperation probePiv() => + RustLib.instance.api.crateApiProtocolProtocolOperationProbePiv(); + /// Start exactly once; subsequent calls report an operation-state error. ProtocolStep start(); } +// Rust type: RustOpaqueMoi> +abstract class ProtocolProfile implements RustOpaqueInterface { + ProtocolOperation adminAction({ + required AdminAction kind, + Uint8List? pin, + required List data, + required int index, + required int value, + required bool existing, + }); + + /// `existing` reuses the caller's authorized Admin transaction and forbids + /// a PIN; otherwise the operation SELECTs and verifies the supplied PIN. + ProtocolOperation adminConfigure({ + required AdminConfigurationPatch patch, + Uint8List? pin, + required bool existing, + }); + + /// Read both typed PASS slot configurations (INS 43). `existing` reuses the + /// caller's selected, already authorized Admin transaction and forbids a + /// PIN; otherwise the operation SELECTs and verifies the supplied PIN. + /// Secret slot material is never returned by firmware. + ProtocolOperation adminPassSlots({Uint8List? pin, required bool existing}); + + /// `existing` reuses the caller's selected, already authorized Admin + /// transaction and forbids a PIN; otherwise the operation SELECTs and + /// verifies only an explicitly supplied PIN. + ProtocolOperation adminRead({ + required AdminReadOperation kind, + Uint8List? pin, + required bool existing, + }); + + /// Replace one PASS slot (INS 44): slot 0 = short touch, 1 = long touch; + /// kind 0 = off, 1 = static password (data, append_enter), 2 = HMAC-SHA1 + /// (data = 20-byte key). OATH slots are configured through the OATH + /// applet, never here. The write is validated before any I/O. + ProtocolOperation adminSetPassSlot({ + required int slot, + required int kind, + required List data, + required bool appendEnter, + Uint8List? pin, + required bool existing, + }); + + /// Idempotent local cleanup. Does not alter the card or already-created ops. + void close(); + + /// Bootstrap observations, not an authenticated session or cached credential. + Uint8List? firmware(); + + String? model(); + + ProtocolOperation oathCalculate({ + required List name, + required int kind, + required int algorithm, + Uint8List? challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathCalculateAll({ + required List challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathClearCode({Uint8List? key, Uint8List? challenge}); + + ProtocolOperation oathDelete({ + required List name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathList({ + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathPut({ + required List name, + required int kind, + required int algorithm, + required int digits, + required List secret, + required bool requireTouch, + required bool increasing, + required int initialCounter, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathRename({ + required List old, + required List new_, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathSelect(); + + /// The current key validates in the same operation only when supplied; + /// setting the first code on an unprotected applet passes no old key. + ProtocolOperation oathSetCode({ + Uint8List? oldKey, + required List newKey, + required List challenge, + }); + + /// Mark an existing HOTP credential as the touch keyboard-emulation + /// default: slot 0 = short, 1 = long. The legacy (pre-3.0) single-slot + /// dialect is chosen from the probed profile; a long slot or append-enter + /// there fails construction with InvalidArgument before any I/O. + ProtocolOperation oathSetDefault({ + required int slot, + required bool appendEnter, + required String name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation oathValidate({ + required List key, + required List challenge, + }); + + ProtocolOperation openpgpActivate({required List password}); + + ProtocolOperation openpgpChangePassword({ + required int reference, + required List old, + required List new_, + }); + + ProtocolOperation openpgpGenerateKey({ + required int slot, + required List password, + }); + + ProtocolOperation openpgpLogout({required int reference}); + + ProtocolOperation openpgpPinStatus({required int reference}); + + ProtocolOperation openpgpReadCertificate({required int slot}); + + ProtocolOperation openpgpReadData({required int tag}); + + /// Set PW1/reset-code/PW3 retry limits after explicit PW3 verification. + /// The card resets PW1/PW3 to firmware defaults and clears authorization; + /// the retry-reset capability gates construction before any I/O. + ProtocolOperation openpgpResetRetries({ + required List retries, + required List password, + }); + + ProtocolOperation openpgpTerminate({required List password}); + + /// Reset PW1 after explicit PW3 verification; does not change PW3. + ProtocolOperation openpgpUnblockWithAdmin({ + required List newPin, + required List password, + }); + + /// Reset PW1 using reset-code||new-PW1; no password verification is inserted. + ProtocolOperation openpgpUnblockWithCode({ + required List code, + required List newPin, + }); + + ProtocolOperation openpgpVerify({ + required int reference, + required List password, + }); + + ProtocolOperation openpgpWriteCertificate({ + required int slot, + required List certificate, + required List password, + }); + + ProtocolOperation openpgpWriteLanguage({ + required List language, + required List password, + }); + + ProtocolOperation openpgpWriteLogin({ + required List login, + required List password, + }); + + ProtocolOperation openpgpWriteName({ + required List name, + required List password, + }); + + /// Set (Some) or clear (None) the reset code after explicit PW3 verification. + ProtocolOperation openpgpWriteResetCode({ + Uint8List? resetCode, + required List password, + }); + + ProtocolOperation openpgpWriteSex({ + required int sex, + required List password, + }); + + /// Whether one explicit PW1-sign verification may authorize multiple + /// signatures (PW status bytes policy). Written after PW3 verification. + ProtocolOperation openpgpWriteSignaturePinPolicy({ + required bool reuse, + required List password, + }); + + /// Touch cache duration in seconds after PW3 verification; UIF-gated. + ProtocolOperation openpgpWriteTouchCacheTime({ + required int seconds, + required List password, + }); + + /// Slot touch policy (0 off, 1 on, 2 permanent) after PW3 verification. + /// Emits the standard two-byte UIF field; the UIF capability gates it. + ProtocolOperation openpgpWriteTouchPolicy({ + required int slot, + required int policy, + required List password, + }); + + ProtocolOperation openpgpWriteUrl({ + required List url, + required List password, + }); + + /// Resolve an observed key algorithm to Console's fixed display enum IDs. + /// These are NOT device wire IDs and never authorize a key operation. + /// Unknown algorithms and closed profiles return None. + int? pivAlgorithmDisplayId({required int wireId}); + + /// INS F9 attestation certificate for a generated slot. The selected-context + /// factory asserts no credential and consumes no authentication state. + ProtocolOperation pivAttest({required int slot}); + + /// Preserve Console's explicit external authentication mode. libcanokey owns + /// the full challenge exchange and cryptography; no host login is inferred. + ProtocolOperation pivAuthenticateManagement({ + required int algorithm, + required List key, + }); + + /// Reuse the caller's selected transaction; the profile is evidence only. + ProtocolOperation pivCertificate({required int objectId}); + + /// Credentials are copied into upstream zeroizing owners before returning. + /// Success does not turn this immutable profile into an authorization token. + ProtocolOperation pivCredential({ + required PivCredentialOperation kind, + required List current, + required List replacement, + }); + + ProtocolOperation pivDecapsulate({ + required int slot, + required List ciphertext, + }); + + ProtocolOperation pivDecrypt({ + required int slot, + required int algorithm, + required List ciphertext, + }); + + ProtocolOperation pivDeleteCertificate({required int objectId}); + + ProtocolOperation pivDeleteKey({required int slot}); + + ProtocolOperation pivDerive({ + required int slot, + required int algorithm, + required List peer, + }); + + ProtocolOperation pivGenerateKey({ + required int slot, + required int algorithm, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation pivImportEcKey({ + required int slot, + required int algorithm, + required List scalar, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation pivImportEd25519Key({ + required int slot, + required List seed, + required int pinPolicy, + required int touchPolicy, + }); + + /// Import a post-quantum seed (INS FE): kind 0 = ML-DSA-65 (32-byte seed), + /// 1 = ML-KEM-768 (64-byte d||z seed). Semantics match the EC/RSA/Ed25519 + /// imports: Access::Existing inside the caller's reserved transaction. + ProtocolOperation pivImportPqSeed({ + required int slot, + required int kind, + required List seed, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation pivImportRsaKey({ + required int slot, + required int algorithm, + required List p, + required List q, + required List dp, + required List dq, + required List qinv, + required int pinPolicy, + required int touchPolicy, + }); + + /// Read metadata in the caller's already selected PIV session. No probing, + /// SELECT or authentication is inserted, including after VERIFY. + ProtocolOperation pivMetadata({required int reference}); + + /// Read the PIV metadata directory in the caller's selected transaction. + /// The upstream parser validates framing and preserves the raw directory. + ProtocolOperation pivMetadataDirectory(); + + ProtocolOperation pivMoveKey({required int source, required int target}); + + /// Return the normalized object value. libcanokey owns framing and parsing. + ProtocolOperation pivReadObject({required List objectId}); + + ProtocolOperation pivResetPinPukRetries({ + required int pinRetries, + required int pukRetries, + }); + + ProtocolOperation pivSetAlgorithmConfig({required List raw}); + + ProtocolOperation pivSetContainerName({ + required int slot, + required String name, + }); + + ProtocolOperation pivSetManagementKey({ + required int algorithm, + required List key, + required int touch, + required bool updateProtected, + }); + + ProtocolOperation pivSign({ + required int slot, + required int algorithm, + required List input, + required int inputKind, + }); + + /// Firmware streaming signature modes: 0 = ML-DSA-65 with the empty context, + /// 1 = randomized Ed25519 (wire mode FF), 2 = SM2 full message with an + /// optional user ID. Classic digest/padded signing stays with `piv_sign`. + ProtocolOperation pivSignStreaming({ + required int slot, + required int mode, + required List message, + Uint8List? userId, + }); + + ProtocolOperation pivSm2Agreement({ + required int slot, + required int role, + required List peerStatic, + required List peerEphemeral, + Uint8List? userId, + Uint8List? peerId, + required int keyLen, + }); + + /// Writes may be partially applied before failure; never retry or roll back. + /// Existing delegates authorization to the card in the caller-owned lease. + ProtocolOperation pivWriteObject({ + required List objectId, + required List data, + }); + + /// The serial observed during explicit discovery. Missing observations stay absent. + Uint8List? serial(); +} + +enum AdminAction { + verifyPin, + changePin, + keyboardInterface, + keyboardReturn, + keyboardKeymap, + clearKeyboardKeymap, + legacyPivExtensions, + legacyTouch, + writeSm2, + nfc, + resetOpenPgp, + resetPiv, + resetOath, + resetNdef, + resetCtap, + resetPass, + factoryReset, +} + +class AdminConfigurationPatch { + final bool? ledOn; + final bool? ndefReadOnly; + final bool? ndefEnabled; + final bool? webusbLanding; + final int featureMask; + final int featureValues; + + const AdminConfigurationPatch({ + this.ledOn, + this.ndefReadOnly, + this.ndefEnabled, + this.webusbLanding, + required this.featureMask, + required this.featureValues, + }); + + @override + int get hashCode => + ledOn.hashCode ^ + ndefReadOnly.hashCode ^ + ndefEnabled.hashCode ^ + webusbLanding.hashCode ^ + featureMask.hashCode ^ + featureValues.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdminConfigurationPatch && + runtimeType == other.runtimeType && + ledOn == other.ledOn && + ndefReadOnly == other.ndefReadOnly && + ndefEnabled == other.ndefEnabled && + webusbLanding == other.webusbLanding && + featureMask == other.featureMask && + featureValues == other.featureValues; +} + +/// Copy progress before close, including on transport failure or cancellation. +class AdminProgress { + final int confirmedWrites; + final bool reprobeRequired; + + const AdminProgress({ + required this.confirmedWrites, + required this.reprobeRequired, + }); + + @override + int get hashCode => confirmedWrites.hashCode ^ reprobeRequired.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdminProgress && + runtimeType == other.runtimeType && + confirmedWrites == other.confirmedWrites && + reprobeRequired == other.reprobeRequired; +} + +/// Admin operations always SELECT, then verify only an explicitly supplied PIN. +enum AdminReadOperation { + firmware, + model, + serial, + chipId, + coreCommit, + configuration, + flashUsage, + appletUsage, + keyboardLayout, + keyboardKeymap, + nfcStatus, + sm2Configuration, +} + +class AdminResult { + final AdminValueKind kind; + final Uint8List data; + final AdminProgress progress; + + const AdminResult({ + required this.kind, + required this.data, + required this.progress, + }); + + @override + int get hashCode => kind.hashCode ^ data.hashCode ^ progress.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AdminResult && + runtimeType == other.runtimeType && + kind == other.kind && + data == other.data && + progress == other.progress; +} + +enum AdminValueKind { + none, + bytes, + configuration, + legacyConfiguration, + flashUsage, + appletUsage, + nfcStatus, + sm2Configuration, + legacySm2Configuration, + pinStatus, + keyboardLayout, + keyboardKeymap, + passSlots, +} + +/// Connection-bootstrap identity steps; raw Admin commands without a profile. +/// Upstream builders own encoding and safe Le correction; no authentication, +/// no other applet selection and no device observations are created. +enum BootstrapIdentityStep { selectAdmin, serial } + +/// Explicit actions in an already prepared lease; never SELECT or probe. +enum PivCredentialOperation { + verifyPin, + changePin, + changePuk, + unblockPin, + logout, +} + /// Initial PIV operations; reads require an already selected PIV application. -enum PivReadOperation { select, version, algorithmConfiguration } +enum PivReadOperation { select, version, algorithmConfiguration, pinStatus } /// Structured, payload-free protocol failure. Transport errors remain in Dart. class ProtocolError { @@ -73,11 +684,24 @@ class ProtocolStep { final Uint8List? command; final Uint8List? data; final ProtocolError? error; + final ProtocolProfile? profile; + final AdminResult? admin; - const ProtocolStep({this.command, this.data, this.error}); + const ProtocolStep({ + this.command, + this.data, + this.error, + this.profile, + this.admin, + }); @override - int get hashCode => command.hashCode ^ data.hashCode ^ error.hashCode; + int get hashCode => + command.hashCode ^ + data.hashCode ^ + error.hashCode ^ + profile.hashCode ^ + admin.hashCode; @override bool operator ==(Object other) => @@ -86,5 +710,7 @@ class ProtocolStep { runtimeType == other.runtimeType && command == other.command && data == other.data && - error == other.error; + error == other.error && + profile == other.profile && + admin == other.admin; } diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 0dad7ac..232c71e 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.13.0'; @override - int get rustContentHash => -1621666059; + int get rustContentHash => -2116397449; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -81,23 +81,504 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + AdminProgress? crateApiProtocolProtocolOperationAdminProgress({ + required ProtocolOperation that, + }); + ProtocolStep crateApiProtocolProtocolOperationAdvance({ required ProtocolOperation that, required List response, }); + ProtocolOperation crateApiProtocolProtocolOperationBootstrapIdentity({ + required BootstrapIdentityStep step, + }); + void crateApiProtocolProtocolOperationClose({ required ProtocolOperation that, }); + ProtocolOperation crateApiProtocolProtocolOperationCtapSelectApplication(); + + ProtocolOperation crateApiProtocolProtocolOperationCtapTransceive({ + required List message, + }); + + ProtocolOperation crateApiProtocolProtocolOperationCtapTransceiveSelected({ + required List message, + }); + + ProtocolOperation crateApiProtocolProtocolOperationNdefReadCapability(); + + ProtocolOperation crateApiProtocolProtocolOperationNdefReadMessage(); + + ProtocolOperation crateApiProtocolProtocolOperationNdefWriteMessage({ + required List message, + }); + ProtocolOperation crateApiProtocolProtocolOperationPivRead({ required PivReadOperation kind, }); + ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin(); + + ProtocolOperation crateApiProtocolProtocolOperationProbePiv(); + ProtocolStep crateApiProtocolProtocolOperationStart({ required ProtocolOperation that, }); + ProtocolOperation crateApiProtocolProtocolProfileAdminAction({ + required ProtocolProfile that, + required AdminAction kind, + Uint8List? pin, + required List data, + required int index, + required int value, + required bool existing, + }); + + ProtocolOperation crateApiProtocolProtocolProfileAdminConfigure({ + required ProtocolProfile that, + required AdminConfigurationPatch patch, + Uint8List? pin, + required bool existing, + }); + + ProtocolOperation crateApiProtocolProtocolProfileAdminPassSlots({ + required ProtocolProfile that, + Uint8List? pin, + required bool existing, + }); + + ProtocolOperation crateApiProtocolProtocolProfileAdminRead({ + required ProtocolProfile that, + required AdminReadOperation kind, + Uint8List? pin, + required bool existing, + }); + + ProtocolOperation crateApiProtocolProtocolProfileAdminSetPassSlot({ + required ProtocolProfile that, + required int slot, + required int kind, + required List data, + required bool appendEnter, + Uint8List? pin, + required bool existing, + }); + + void crateApiProtocolProtocolProfileClose({required ProtocolProfile that}); + + Uint8List? crateApiProtocolProtocolProfileFirmware({ + required ProtocolProfile that, + }); + + String? crateApiProtocolProtocolProfileModel({required ProtocolProfile that}); + + ProtocolOperation crateApiProtocolProtocolProfileOathCalculate({ + required ProtocolProfile that, + required List name, + required int kind, + required int algorithm, + Uint8List? challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathCalculateAll({ + required ProtocolProfile that, + required List challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathClearCode({ + required ProtocolProfile that, + Uint8List? key, + Uint8List? challenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathDelete({ + required ProtocolProfile that, + required List name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathList({ + required ProtocolProfile that, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathPut({ + required ProtocolProfile that, + required List name, + required int kind, + required int algorithm, + required int digits, + required List secret, + required bool requireTouch, + required bool increasing, + required int initialCounter, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathRename({ + required ProtocolProfile that, + required List old, + required List new_, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathSelect({ + required ProtocolProfile that, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathSetCode({ + required ProtocolProfile that, + Uint8List? oldKey, + required List newKey, + required List challenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathSetDefault({ + required ProtocolProfile that, + required int slot, + required bool appendEnter, + required String name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOathValidate({ + required ProtocolProfile that, + required List key, + required List challenge, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpActivate({ + required ProtocolProfile that, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpChangePassword({ + required ProtocolProfile that, + required int reference, + required List old, + required List new_, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpGenerateKey({ + required ProtocolProfile that, + required int slot, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpLogout({ + required ProtocolProfile that, + required int reference, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpPinStatus({ + required ProtocolProfile that, + required int reference, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpReadCertificate({ + required ProtocolProfile that, + required int slot, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpReadData({ + required ProtocolProfile that, + required int tag, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpResetRetries({ + required ProtocolProfile that, + required List retries, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpTerminate({ + required ProtocolProfile that, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpUnblockWithAdmin({ + required ProtocolProfile that, + required List newPin, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpUnblockWithCode({ + required ProtocolProfile that, + required List code, + required List newPin, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpVerify({ + required ProtocolProfile that, + required int reference, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteCertificate({ + required ProtocolProfile that, + required int slot, + required List certificate, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteLanguage({ + required ProtocolProfile that, + required List language, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteLogin({ + required ProtocolProfile that, + required List login, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteName({ + required ProtocolProfile that, + required List name, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteResetCode({ + required ProtocolProfile that, + Uint8List? resetCode, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteSex({ + required ProtocolProfile that, + required int sex, + required List password, + }); + + ProtocolOperation + crateApiProtocolProtocolProfileOpenpgpWriteSignaturePinPolicy({ + required ProtocolProfile that, + required bool reuse, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteTouchCacheTime({ + required ProtocolProfile that, + required int seconds, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteTouchPolicy({ + required ProtocolProfile that, + required int slot, + required int policy, + required List password, + }); + + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteUrl({ + required ProtocolProfile that, + required List url, + required List password, + }); + + int? crateApiProtocolProtocolProfilePivAlgorithmDisplayId({ + required ProtocolProfile that, + required int wireId, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivAttest({ + required ProtocolProfile that, + required int slot, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivAuthenticateManagement({ + required ProtocolProfile that, + required int algorithm, + required List key, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivCertificate({ + required ProtocolProfile that, + required int objectId, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivCredential({ + required ProtocolProfile that, + required PivCredentialOperation kind, + required List current, + required List replacement, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivDecapsulate({ + required ProtocolProfile that, + required int slot, + required List ciphertext, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivDecrypt({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List ciphertext, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivDeleteCertificate({ + required ProtocolProfile that, + required int objectId, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivDeleteKey({ + required ProtocolProfile that, + required int slot, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivDerive({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List peer, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivGenerateKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivImportEcKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List scalar, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivImportEd25519Key({ + required ProtocolProfile that, + required int slot, + required List seed, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivImportPqSeed({ + required ProtocolProfile that, + required int slot, + required int kind, + required List seed, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivImportRsaKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List p, + required List q, + required List dp, + required List dq, + required List qinv, + required int pinPolicy, + required int touchPolicy, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivMetadata({ + required ProtocolProfile that, + required int reference, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivMetadataDirectory({ + required ProtocolProfile that, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivMoveKey({ + required ProtocolProfile that, + required int source, + required int target, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivReadObject({ + required ProtocolProfile that, + required List objectId, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivResetPinPukRetries({ + required ProtocolProfile that, + required int pinRetries, + required int pukRetries, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSetAlgorithmConfig({ + required ProtocolProfile that, + required List raw, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSetContainerName({ + required ProtocolProfile that, + required int slot, + required String name, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSetManagementKey({ + required ProtocolProfile that, + required int algorithm, + required List key, + required int touch, + required bool updateProtected, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSign({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List input, + required int inputKind, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSignStreaming({ + required ProtocolProfile that, + required int slot, + required int mode, + required List message, + Uint8List? userId, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivSm2Agreement({ + required ProtocolProfile that, + required int slot, + required int role, + required List peerStatic, + required List peerEphemeral, + Uint8List? userId, + Uint8List? peerId, + required int keyLen, + }); + + ProtocolOperation crateApiProtocolProtocolProfilePivWriteObject({ + required ProtocolProfile that, + required List objectId, + required List data, + }); + + Uint8List? crateApiProtocolProtocolProfileSerial({ + required ProtocolProfile that, + }); + PivPublicKeyData crateApiPivCryptoBuildPivPublicKey({ required int algorithm, required List cardData, @@ -167,178 +648,3179 @@ abstract class RustLibApi extends BaseApi { required List subjectAlternativeNames, }); - Uint8List crateApiPivCryptoPreparePivSigningInput({ - required int algorithm, - required List data, - Uint8List? publicKey, - }); + Uint8List crateApiPivCryptoPreparePivSigningInput({ + required int algorithm, + required List data, + Uint8List? publicKey, + }); + + Uint8List crateApiPivCryptoPrepareSelfSignedCertificate({ + required SelfSignedCertificateParams params, + }); + + Uint8List crateApiCryptoSha256Digest({required List data}); + + Uint8List crateApiCryptoSha384Digest({required List data}); + + Uint8List crateApiCryptoSha512Digest({required List data}); + + Uint8List crateApiCryptoSm2MessageDigest({ + required List data, + required List publicKey, + }); + + Uint8List crateApiCryptoTdesEde3Enc({ + required List key, + required List data, + }); + + bool crateApiCryptoVerifyPivSignature({ + required int algorithm, + required List publicKey, + required List data, + required List signature, + }); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_ProtocolOperation; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_ProtocolOperation; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolOperationPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_ProtocolProfile; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_ProtocolProfile; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolProfilePtr; +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + AdminProgress? crateApiProtocolProtocolOperationAdminProgress({ + required ProtocolOperation that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_admin_progress, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationAdminProgressConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationAdminProgressConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_admin_progress", + argNames: ["that"], + ); + + @override + ProtocolStep crateApiProtocolProtocolOperationAdvance({ + required ProtocolOperation that, + required List response, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(response, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_protocol_step, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationAdvanceConstMeta, + argValues: [that, response], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationAdvanceConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_advance", + argNames: ["that", "response"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationBootstrapIdentity({ + required BootstrapIdentityStep step, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_bootstrap_identity_step(step, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationBootstrapIdentityConstMeta, + argValues: [step], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationBootstrapIdentityConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_bootstrap_identity", + argNames: ["step"], + ); + + @override + void crateApiProtocolProtocolOperationClose({ + required ProtocolOperation that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationCloseConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_close", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationCtapSelectApplication() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolOperationCtapSelectApplicationConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationCtapSelectApplicationConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ctap_select_application", + argNames: [], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationCtapTransceive({ + required List message, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(message, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationCtapTransceiveConstMeta, + argValues: [message], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationCtapTransceiveConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ctap_transceive", + argNames: ["message"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationCtapTransceiveSelected({ + required List message, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(message, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolOperationCtapTransceiveSelectedConstMeta, + argValues: [message], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationCtapTransceiveSelectedConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ctap_transceive_selected", + argNames: ["message"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationNdefReadCapability() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolOperationNdefReadCapabilityConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationNdefReadCapabilityConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ndef_read_capability", + argNames: [], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationNdefReadMessage() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationNdefReadMessageConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationNdefReadMessageConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ndef_read_message", + argNames: [], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationNdefWriteMessage({ + required List message, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(message, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationNdefWriteMessageConstMeta, + argValues: [message], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationNdefWriteMessageConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ndef_write_message", + argNames: ["message"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationPivRead({ + required PivReadOperation kind, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_piv_read_operation(kind, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationPivReadConstMeta, + argValues: [kind], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationPivReadConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_piv_read", + argNames: ["kind"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationProbeAdminConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationProbeAdminConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_probe_admin", + argNames: [], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationProbePiv() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationProbePivConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationProbePivConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_probe_piv", + argNames: [], + ); + + @override + ProtocolStep crateApiProtocolProtocolOperationStart({ + required ProtocolOperation that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_protocol_step, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationStartConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationStartConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_start", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileAdminAction({ + required ProtocolProfile that, + required AdminAction kind, + Uint8List? pin, + required List data, + required int index, + required int value, + required bool existing, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_admin_action(kind, serializer); + sse_encode_opt_list_prim_u_8_strict(pin, serializer); + sse_encode_list_prim_u_8_loose(data, serializer); + sse_encode_u_8(index, serializer); + sse_encode_u_8(value, serializer); + sse_encode_bool(existing, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileAdminActionConstMeta, + argValues: [that, kind, pin, data, index, value, existing], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileAdminActionConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_admin_action", + argNames: ["that", "kind", "pin", "data", "index", "value", "existing"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileAdminConfigure({ + required ProtocolProfile that, + required AdminConfigurationPatch patch, + Uint8List? pin, + required bool existing, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_box_autoadd_admin_configuration_patch(patch, serializer); + sse_encode_opt_list_prim_u_8_strict(pin, serializer); + sse_encode_bool(existing, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileAdminConfigureConstMeta, + argValues: [that, patch, pin, existing], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileAdminConfigureConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_admin_configure", + argNames: ["that", "patch", "pin", "existing"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileAdminPassSlots({ + required ProtocolProfile that, + Uint8List? pin, + required bool existing, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(pin, serializer); + sse_encode_bool(existing, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileAdminPassSlotsConstMeta, + argValues: [that, pin, existing], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileAdminPassSlotsConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_admin_pass_slots", + argNames: ["that", "pin", "existing"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileAdminRead({ + required ProtocolProfile that, + required AdminReadOperation kind, + Uint8List? pin, + required bool existing, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_admin_read_operation(kind, serializer); + sse_encode_opt_list_prim_u_8_strict(pin, serializer); + sse_encode_bool(existing, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileAdminReadConstMeta, + argValues: [that, kind, pin, existing], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileAdminReadConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_admin_read", + argNames: ["that", "kind", "pin", "existing"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileAdminSetPassSlot({ + required ProtocolProfile that, + required int slot, + required int kind, + required List data, + required bool appendEnter, + Uint8List? pin, + required bool existing, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(kind, serializer); + sse_encode_list_prim_u_8_loose(data, serializer); + sse_encode_bool(appendEnter, serializer); + sse_encode_opt_list_prim_u_8_strict(pin, serializer); + sse_encode_bool(existing, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileAdminSetPassSlotConstMeta, + argValues: [that, slot, kind, data, appendEnter, pin, existing], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileAdminSetPassSlotConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_admin_set_pass_slot", + argNames: [ + "that", + "slot", + "kind", + "data", + "appendEnter", + "pin", + "existing", + ], + ); + + @override + void crateApiProtocolProtocolProfileClose({required ProtocolProfile that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileCloseConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_close", + argNames: ["that"], + ); + + @override + Uint8List? crateApiProtocolProtocolProfileFirmware({ + required ProtocolProfile that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileFirmwareConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileFirmwareConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_firmware", + argNames: ["that"], + ); + + @override + String? crateApiProtocolProtocolProfileModel({ + required ProtocolProfile that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_String, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileModelConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileModelConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_model", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathCalculate({ + required ProtocolProfile that, + required List name, + required int kind, + required int algorithm, + Uint8List? challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(name, serializer); + sse_encode_u_8(kind, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_opt_list_prim_u_8_strict(challenge, serializer); + sse_encode_u_8(format, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathCalculateConstMeta, + argValues: [ + that, + name, + kind, + algorithm, + challenge, + format, + accessKey, + accessChallenge, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathCalculateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_calculate", + argNames: [ + "that", + "name", + "kind", + "algorithm", + "challenge", + "format", + "accessKey", + "accessChallenge", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathCalculateAll({ + required ProtocolProfile that, + required List challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(challenge, serializer); + sse_encode_u_8(format, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathCalculateAllConstMeta, + argValues: [that, challenge, format, accessKey, accessChallenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathCalculateAllConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_calculate_all", + argNames: [ + "that", + "challenge", + "format", + "accessKey", + "accessChallenge", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathClearCode({ + required ProtocolProfile that, + Uint8List? key, + Uint8List? challenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(key, serializer); + sse_encode_opt_list_prim_u_8_strict(challenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathClearCodeConstMeta, + argValues: [that, key, challenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathClearCodeConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_clear_code", + argNames: ["that", "key", "challenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathDelete({ + required ProtocolProfile that, + required List name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(name, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathDeleteConstMeta, + argValues: [that, name, accessKey, accessChallenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathDeleteConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_delete", + argNames: ["that", "name", "accessKey", "accessChallenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathList({ + required ProtocolProfile that, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathListConstMeta, + argValues: [that, accessKey, accessChallenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathListConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_list", + argNames: ["that", "accessKey", "accessChallenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathPut({ + required ProtocolProfile that, + required List name, + required int kind, + required int algorithm, + required int digits, + required List secret, + required bool requireTouch, + required bool increasing, + required int initialCounter, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(name, serializer); + sse_encode_u_8(kind, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_u_8(digits, serializer); + sse_encode_list_prim_u_8_loose(secret, serializer); + sse_encode_bool(requireTouch, serializer); + sse_encode_bool(increasing, serializer); + sse_encode_u_32(initialCounter, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathPutConstMeta, + argValues: [ + that, + name, + kind, + algorithm, + digits, + secret, + requireTouch, + increasing, + initialCounter, + accessKey, + accessChallenge, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathPutConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_put", + argNames: [ + "that", + "name", + "kind", + "algorithm", + "digits", + "secret", + "requireTouch", + "increasing", + "initialCounter", + "accessKey", + "accessChallenge", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathRename({ + required ProtocolProfile that, + required List old, + required List new_, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(old, serializer); + sse_encode_list_prim_u_8_loose(new_, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathRenameConstMeta, + argValues: [that, old, new_, accessKey, accessChallenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathRenameConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_rename", + argNames: ["that", "old", "new_", "accessKey", "accessChallenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathSelect({ + required ProtocolProfile that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathSelectConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathSelectConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_select", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathSetCode({ + required ProtocolProfile that, + Uint8List? oldKey, + required List newKey, + required List challenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(oldKey, serializer); + sse_encode_list_prim_u_8_loose(newKey, serializer); + sse_encode_list_prim_u_8_loose(challenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathSetCodeConstMeta, + argValues: [that, oldKey, newKey, challenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathSetCodeConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_set_code", + argNames: ["that", "oldKey", "newKey", "challenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathSetDefault({ + required ProtocolProfile that, + required int slot, + required bool appendEnter, + required String name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_bool(appendEnter, serializer); + sse_encode_String(name, serializer); + sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); + sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathSetDefaultConstMeta, + argValues: [that, slot, appendEnter, name, accessKey, accessChallenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathSetDefaultConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_set_default", + argNames: [ + "that", + "slot", + "appendEnter", + "name", + "accessKey", + "accessChallenge", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOathValidate({ + required ProtocolProfile that, + required List key, + required List challenge, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(key, serializer); + sse_encode_list_prim_u_8_loose(challenge, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOathValidateConstMeta, + argValues: [that, key, challenge], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOathValidateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_oath_validate", + argNames: ["that", "key", "challenge"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpActivate({ + required ProtocolProfile that, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpActivateConstMeta, + argValues: [that, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpActivateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_activate", + argNames: ["that", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpChangePassword({ + required ProtocolProfile that, + required int reference, + required List old, + required List new_, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(reference, serializer); + sse_encode_list_prim_u_8_loose(old, serializer); + sse_encode_list_prim_u_8_loose(new_, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpChangePasswordConstMeta, + argValues: [that, reference, old, new_], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpChangePasswordConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_change_password", + argNames: ["that", "reference", "old", "new_"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpGenerateKey({ + required ProtocolProfile that, + required int slot, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpGenerateKeyConstMeta, + argValues: [that, slot, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpGenerateKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_generate_key", + argNames: ["that", "slot", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpLogout({ + required ProtocolProfile that, + required int reference, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(reference, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpLogoutConstMeta, + argValues: [that, reference], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpLogoutConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_logout", + argNames: ["that", "reference"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpPinStatus({ + required ProtocolProfile that, + required int reference, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(reference, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpPinStatusConstMeta, + argValues: [that, reference], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpPinStatusConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_pin_status", + argNames: ["that", "reference"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpReadCertificate({ + required ProtocolProfile that, + required int slot, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpReadCertificateConstMeta, + argValues: [that, slot], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpReadCertificateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_read_certificate", + argNames: ["that", "slot"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpReadData({ + required ProtocolProfile that, + required int tag, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_16(tag, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpReadDataConstMeta, + argValues: [that, tag], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpReadDataConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_read_data", + argNames: ["that", "tag"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpResetRetries({ + required ProtocolProfile that, + required List retries, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(retries, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 41)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpResetRetriesConstMeta, + argValues: [that, retries, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpResetRetriesConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_reset_retries", + argNames: ["that", "retries", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpTerminate({ + required ProtocolProfile that, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpTerminateConstMeta, + argValues: [that, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpTerminateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_terminate", + argNames: ["that", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpUnblockWithAdmin({ + required ProtocolProfile that, + required List newPin, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(newPin, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpUnblockWithAdminConstMeta, + argValues: [that, newPin, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpUnblockWithAdminConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_unblock_with_admin", + argNames: ["that", "newPin", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpUnblockWithCode({ + required ProtocolProfile that, + required List code, + required List newPin, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(code, serializer); + sse_encode_list_prim_u_8_loose(newPin, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpUnblockWithCodeConstMeta, + argValues: [that, code, newPin], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpUnblockWithCodeConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_unblock_with_code", + argNames: ["that", "code", "newPin"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpVerify({ + required ProtocolProfile that, + required int reference, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(reference, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpVerifyConstMeta, + argValues: [that, reference, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpVerifyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_verify", + argNames: ["that", "reference", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteCertificate({ + required ProtocolProfile that, + required int slot, + required List certificate, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_list_prim_u_8_loose(certificate, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteCertificateConstMeta, + argValues: [that, slot, certificate, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteCertificateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_certificate", + argNames: ["that", "slot", "certificate", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteLanguage({ + required ProtocolProfile that, + required List language, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(language, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteLanguageConstMeta, + argValues: [that, language, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteLanguageConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_language", + argNames: ["that", "language", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteLogin({ + required ProtocolProfile that, + required List login, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(login, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpWriteLoginConstMeta, + argValues: [that, login, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteLoginConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_login", + argNames: ["that", "login", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteName({ + required ProtocolProfile that, + required List name, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(name, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 49)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpWriteNameConstMeta, + argValues: [that, name, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpWriteNameConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_name", + argNames: ["that", "name", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteResetCode({ + required ProtocolProfile that, + Uint8List? resetCode, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(resetCode, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 50)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteResetCodeConstMeta, + argValues: [that, resetCode, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteResetCodeConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_reset_code", + argNames: ["that", "resetCode", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteSex({ + required ProtocolProfile that, + required int sex, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(sex, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpWriteSexConstMeta, + argValues: [that, sex, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpWriteSexConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_sex", + argNames: ["that", "sex", "password"], + ); + + @override + ProtocolOperation + crateApiProtocolProtocolProfileOpenpgpWriteSignaturePinPolicy({ + required ProtocolProfile that, + required bool reuse, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_bool(reuse, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 52)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteSignaturePinPolicyConstMeta, + argValues: [that, reuse, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteSignaturePinPolicyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_signature_pin_policy", + argNames: ["that", "reuse", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteTouchCacheTime({ + required ProtocolProfile that, + required int seconds, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(seconds, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteTouchCacheTimeConstMeta, + argValues: [that, seconds, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteTouchCacheTimeConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_touch_cache_time", + argNames: ["that", "seconds", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteTouchPolicy({ + required ProtocolProfile that, + required int slot, + required int policy, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(policy, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfileOpenpgpWriteTouchPolicyConstMeta, + argValues: [that, slot, policy, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfileOpenpgpWriteTouchPolicyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_touch_policy", + argNames: ["that", "slot", "policy", "password"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfileOpenpgpWriteUrl({ + required ProtocolProfile that, + required List url, + required List password, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(url, serializer); + sse_encode_list_prim_u_8_loose(password, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfileOpenpgpWriteUrlConstMeta, + argValues: [that, url, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfileOpenpgpWriteUrlConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_openpgp_write_url", + argNames: ["that", "url", "password"], + ); + + @override + int? crateApiProtocolProtocolProfilePivAlgorithmDisplayId({ + required ProtocolProfile that, + required int wireId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(wireId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 56)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_u_8, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivAlgorithmDisplayIdConstMeta, + argValues: [that, wireId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivAlgorithmDisplayIdConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_algorithm_display_id", + argNames: ["that", "wireId"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivAttest({ + required ProtocolProfile that, + required int slot, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 57)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivAttestConstMeta, + argValues: [that, slot], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivAttestConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_attest", + argNames: ["that", "slot"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivAuthenticateManagement({ + required ProtocolProfile that, + required int algorithm, + required List key, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(key, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 58)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivAuthenticateManagementConstMeta, + argValues: [that, algorithm, key], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivAuthenticateManagementConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_authenticate_management", + argNames: ["that", "algorithm", "key"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivCertificate({ + required ProtocolProfile that, + required int objectId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(objectId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivCertificateConstMeta, + argValues: [that, objectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivCertificateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_certificate", + argNames: ["that", "objectId"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivCredential({ + required ProtocolProfile that, + required PivCredentialOperation kind, + required List current, + required List replacement, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_piv_credential_operation(kind, serializer); + sse_encode_list_prim_u_8_loose(current, serializer); + sse_encode_list_prim_u_8_loose(replacement, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 60)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivCredentialConstMeta, + argValues: [that, kind, current, replacement], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivCredentialConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_credential", + argNames: ["that", "kind", "current", "replacement"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivDecapsulate({ + required ProtocolProfile that, + required int slot, + required List ciphertext, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_list_prim_u_8_loose(ciphertext, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 61)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivDecapsulateConstMeta, + argValues: [that, slot, ciphertext], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivDecapsulateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_decapsulate", + argNames: ["that", "slot", "ciphertext"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivDecrypt({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List ciphertext, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(ciphertext, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 62)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivDecryptConstMeta, + argValues: [that, slot, algorithm, ciphertext], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivDecryptConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_decrypt", + argNames: ["that", "slot", "algorithm", "ciphertext"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivDeleteCertificate({ + required ProtocolProfile that, + required int objectId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(objectId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivDeleteCertificateConstMeta, + argValues: [that, objectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivDeleteCertificateConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_delete_certificate", + argNames: ["that", "objectId"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivDeleteKey({ + required ProtocolProfile that, + required int slot, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivDeleteKeyConstMeta, + argValues: [that, slot], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivDeleteKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_delete_key", + argNames: ["that", "slot"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivDerive({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List peer, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(peer, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivDeriveConstMeta, + argValues: [that, slot, algorithm, peer], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivDeriveConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_derive", + argNames: ["that", "slot", "algorithm", "peer"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivGenerateKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required int pinPolicy, + required int touchPolicy, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_u_8(pinPolicy, serializer); + sse_encode_u_8(touchPolicy, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivGenerateKeyConstMeta, + argValues: [that, slot, algorithm, pinPolicy, touchPolicy], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivGenerateKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_generate_key", + argNames: ["that", "slot", "algorithm", "pinPolicy", "touchPolicy"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivImportEcKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List scalar, + required int pinPolicy, + required int touchPolicy, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(scalar, serializer); + sse_encode_u_8(pinPolicy, serializer); + sse_encode_u_8(touchPolicy, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivImportEcKeyConstMeta, + argValues: [that, slot, algorithm, scalar, pinPolicy, touchPolicy], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivImportEcKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_import_ec_key", + argNames: [ + "that", + "slot", + "algorithm", + "scalar", + "pinPolicy", + "touchPolicy", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivImportEd25519Key({ + required ProtocolProfile that, + required int slot, + required List seed, + required int pinPolicy, + required int touchPolicy, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_list_prim_u_8_loose(seed, serializer); + sse_encode_u_8(pinPolicy, serializer); + sse_encode_u_8(touchPolicy, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 68)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivImportEd25519KeyConstMeta, + argValues: [that, slot, seed, pinPolicy, touchPolicy], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivImportEd25519KeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_import_ed25519_key", + argNames: ["that", "slot", "seed", "pinPolicy", "touchPolicy"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivImportPqSeed({ + required ProtocolProfile that, + required int slot, + required int kind, + required List seed, + required int pinPolicy, + required int touchPolicy, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(kind, serializer); + sse_encode_list_prim_u_8_loose(seed, serializer); + sse_encode_u_8(pinPolicy, serializer); + sse_encode_u_8(touchPolicy, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivImportPqSeedConstMeta, + argValues: [that, slot, kind, seed, pinPolicy, touchPolicy], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivImportPqSeedConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_import_pq_seed", + argNames: ["that", "slot", "kind", "seed", "pinPolicy", "touchPolicy"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivImportRsaKey({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List p, + required List q, + required List dp, + required List dq, + required List qinv, + required int pinPolicy, + required int touchPolicy, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(p, serializer); + sse_encode_list_prim_u_8_loose(q, serializer); + sse_encode_list_prim_u_8_loose(dp, serializer); + sse_encode_list_prim_u_8_loose(dq, serializer); + sse_encode_list_prim_u_8_loose(qinv, serializer); + sse_encode_u_8(pinPolicy, serializer); + sse_encode_u_8(touchPolicy, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivImportRsaKeyConstMeta, + argValues: [ + that, + slot, + algorithm, + p, + q, + dp, + dq, + qinv, + pinPolicy, + touchPolicy, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivImportRsaKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_import_rsa_key", + argNames: [ + "that", + "slot", + "algorithm", + "p", + "q", + "dp", + "dq", + "qinv", + "pinPolicy", + "touchPolicy", + ], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivMetadata({ + required ProtocolProfile that, + required int reference, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(reference, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 71)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivMetadataConstMeta, + argValues: [that, reference], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivMetadataConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_metadata", + argNames: ["that", "reference"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivMetadataDirectory({ + required ProtocolProfile that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 72)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivMetadataDirectoryConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivMetadataDirectoryConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_metadata_directory", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivMoveKey({ + required ProtocolProfile that, + required int source, + required int target, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(source, serializer); + sse_encode_u_8(target, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 73)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivMoveKeyConstMeta, + argValues: [that, source, target], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivMoveKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_move_key", + argNames: ["that", "source", "target"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolProfilePivReadObject({ + required ProtocolProfile that, + required List objectId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(objectId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 74)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivReadObjectConstMeta, + argValues: [that, objectId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolProfilePivReadObjectConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_read_object", + argNames: ["that", "objectId"], + ); - Uint8List crateApiPivCryptoPrepareSelfSignedCertificate({ - required SelfSignedCertificateParams params, - }); + @override + ProtocolOperation crateApiProtocolProtocolProfilePivResetPinPukRetries({ + required ProtocolProfile that, + required int pinRetries, + required int pukRetries, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(pinRetries, serializer); + sse_encode_u_8(pukRetries, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 75)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivResetPinPukRetriesConstMeta, + argValues: [that, pinRetries, pukRetries], + apiImpl: this, + ), + ); + } - Uint8List crateApiCryptoSha256Digest({required List data}); + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivResetPinPukRetriesConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_reset_pin_puk_retries", + argNames: ["that", "pinRetries", "pukRetries"], + ); - Uint8List crateApiCryptoSha384Digest({required List data}); + @override + ProtocolOperation crateApiProtocolProtocolProfilePivSetAlgorithmConfig({ + required ProtocolProfile that, + required List raw, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(raw, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 76)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivSetAlgorithmConfigConstMeta, + argValues: [that, raw], + apiImpl: this, + ), + ); + } - Uint8List crateApiCryptoSha512Digest({required List data}); + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivSetAlgorithmConfigConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_set_algorithm_config", + argNames: ["that", "raw"], + ); - Uint8List crateApiCryptoSm2MessageDigest({ - required List data, - required List publicKey, - }); + @override + ProtocolOperation crateApiProtocolProtocolProfilePivSetContainerName({ + required ProtocolProfile that, + required int slot, + required String name, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_String(name, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivSetContainerNameConstMeta, + argValues: [that, slot, name], + apiImpl: this, + ), + ); + } - Uint8List crateApiCryptoTdesEde3Enc({ - required List key, - required List data, - }); + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivSetContainerNameConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_set_container_name", + argNames: ["that", "slot", "name"], + ); - bool crateApiCryptoVerifyPivSignature({ + @override + ProtocolOperation crateApiProtocolProtocolProfilePivSetManagementKey({ + required ProtocolProfile that, required int algorithm, - required List publicKey, - required List data, - required List signature, - }); - - RustArcIncrementStrongCountFnType - get rust_arc_increment_strong_count_ProtocolOperation; + required List key, + required int touch, + required bool updateProtected, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(key, serializer); + sse_encode_u_8(touch, serializer); + sse_encode_bool(updateProtected, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivSetManagementKeyConstMeta, + argValues: [that, algorithm, key, touch, updateProtected], + apiImpl: this, + ), + ); + } - RustArcDecrementStrongCountFnType - get rust_arc_decrement_strong_count_ProtocolOperation; + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivSetManagementKeyConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_set_management_key", + argNames: ["that", "algorithm", "key", "touch", "updateProtected"], + ); - CrossPlatformFinalizerArg - get rust_arc_decrement_strong_count_ProtocolOperationPtr; -} + @override + ProtocolOperation crateApiProtocolProtocolProfilePivSign({ + required ProtocolProfile that, + required int slot, + required int algorithm, + required List input, + required int inputKind, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_u_8(slot, serializer); + sse_encode_u_8(algorithm, serializer); + sse_encode_list_prim_u_8_loose(input, serializer); + sse_encode_u_8(inputKind, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 79)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolProfilePivSignConstMeta, + argValues: [that, slot, algorithm, input, inputKind], + apiImpl: this, + ), + ); + } -class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { - RustLibApiImpl({ - required super.handler, - required super.wire, - required super.generalizedFrbRustBinding, - required super.portManager, - }); + TaskConstMeta get kCrateApiProtocolProtocolProfilePivSignConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_sign", + argNames: ["that", "slot", "algorithm", "input", "inputKind"], + ); @override - ProtocolStep crateApiProtocolProtocolOperationAdvance({ - required ProtocolOperation that, - required List response, + ProtocolOperation crateApiProtocolProtocolProfilePivSignStreaming({ + required ProtocolProfile that, + required int slot, + required int mode, + required List message, + Uint8List? userId, }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( that, serializer, ); - sse_encode_list_prim_u_8_loose(response, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + sse_encode_u_8(slot, serializer); + sse_encode_u_8(mode, serializer); + sse_encode_list_prim_u_8_loose(message, serializer); + sse_encode_opt_list_prim_u_8_strict(userId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 80)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_protocol_step, + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, decodeErrorData: null, ), - constMeta: kCrateApiProtocolProtocolOperationAdvanceConstMeta, - argValues: [that, response], + constMeta: kCrateApiProtocolProtocolProfilePivSignStreamingConstMeta, + argValues: [that, slot, mode, message, userId], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiProtocolProtocolOperationAdvanceConstMeta => + TaskConstMeta get kCrateApiProtocolProtocolProfilePivSignStreamingConstMeta => const TaskConstMeta( - debugName: "ProtocolOperation_advance", - argNames: ["that", "response"], + debugName: "ProtocolProfile_piv_sign_streaming", + argNames: ["that", "slot", "mode", "message", "userId"], ); @override - void crateApiProtocolProtocolOperationClose({ - required ProtocolOperation that, + ProtocolOperation crateApiProtocolProtocolProfilePivSm2Agreement({ + required ProtocolProfile that, + required int slot, + required int role, + required List peerStatic, + required List peerEphemeral, + Uint8List? userId, + Uint8List? peerId, + required int keyLen, }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + sse_encode_u_8(slot, serializer); + sse_encode_u_8(role, serializer); + sse_encode_list_prim_u_8_loose(peerStatic, serializer); + sse_encode_list_prim_u_8_loose(peerEphemeral, serializer); + sse_encode_opt_list_prim_u_8_strict(userId, serializer); + sse_encode_opt_list_prim_u_8_strict(peerId, serializer); + sse_encode_u_16(keyLen, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 81)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_unit, + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, decodeErrorData: null, ), - constMeta: kCrateApiProtocolProtocolOperationCloseConstMeta, - argValues: [that], + constMeta: kCrateApiProtocolProtocolProfilePivSm2AgreementConstMeta, + argValues: [ + that, + slot, + role, + peerStatic, + peerEphemeral, + userId, + peerId, + keyLen, + ], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiProtocolProtocolOperationCloseConstMeta => + TaskConstMeta get kCrateApiProtocolProtocolProfilePivSm2AgreementConstMeta => const TaskConstMeta( - debugName: "ProtocolOperation_close", - argNames: ["that"], + debugName: "ProtocolProfile_piv_sm2_agreement", + argNames: [ + "that", + "slot", + "role", + "peerStatic", + "peerEphemeral", + "userId", + "peerId", + "keyLen", + ], ); @override - ProtocolOperation crateApiProtocolProtocolOperationPivRead({ - required PivReadOperation kind, + ProtocolOperation crateApiProtocolProtocolProfilePivWriteObject({ + required ProtocolProfile that, + required List objectId, + required List data, }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_piv_read_operation(kind, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(objectId, serializer); + sse_encode_list_prim_u_8_loose(data, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 82)!; }, codec: SseCodec( decodeSuccessData: sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, decodeErrorData: null, ), - constMeta: kCrateApiProtocolProtocolOperationPivReadConstMeta, - argValues: [kind], + constMeta: kCrateApiProtocolProtocolProfilePivWriteObjectConstMeta, + argValues: [that, objectId, data], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiProtocolProtocolOperationPivReadConstMeta => + TaskConstMeta get kCrateApiProtocolProtocolProfilePivWriteObjectConstMeta => const TaskConstMeta( - debugName: "ProtocolOperation_piv_read", - argNames: ["kind"], + debugName: "ProtocolProfile_piv_write_object", + argNames: ["that", "objectId", "data"], ); @override - ProtocolStep crateApiProtocolProtocolOperationStart({ - required ProtocolOperation that, + Uint8List? crateApiProtocolProtocolProfileSerial({ + required ProtocolProfile that, }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; }, codec: SseCodec( - decodeSuccessData: sse_decode_protocol_step, + decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, decodeErrorData: null, ), - constMeta: kCrateApiProtocolProtocolOperationStartConstMeta, + constMeta: kCrateApiProtocolProtocolProfileSerialConstMeta, argValues: [that], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiProtocolProtocolOperationStartConstMeta => + TaskConstMeta get kCrateApiProtocolProtocolProfileSerialConstMeta => const TaskConstMeta( - debugName: "ProtocolOperation_start", + debugName: "ProtocolProfile_serial", argNames: ["that"], ); @@ -355,7 +3837,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(cardData, serializer); sse_encode_bool(generatedResponse, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 84)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -381,7 +3863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(pngFile, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 85)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -413,7 +3895,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -445,7 +3927,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(certificationRequestInfo, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -477,7 +3959,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(tbsCertificate, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -507,7 +3989,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -529,7 +4011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 90)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -554,7 +4036,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(bytes, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_import_file_data, @@ -584,7 +4066,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 92)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -610,7 +4092,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(der, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -636,7 +4118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(pem, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -670,7 +4152,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(salt, serializer); sse_encode_u_32(iterations, serializer); sse_encode_u_32(keyLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -704,7 +4186,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(expectedPublicKey, serializer); sse_encode_u_8(slot, serializer); sse_encode_i_64(nowUnix, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -742,7 +4224,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(country, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); sse_encode_list_String(subjectAlternativeNames, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -788,7 +4270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_opt_list_prim_u_8_strict(publicKey, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -819,7 +4301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { params, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 99)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -845,7 +4327,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 100, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -868,7 +4354,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 101, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -891,7 +4381,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 102, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -918,7 +4412,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_list_prim_u_8_loose(publicKey, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 103, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -948,7 +4446,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 104, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -981,7 +4483,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(publicKey, serializer); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 105, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -1008,13 +4514,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { get rust_arc_decrement_strong_count_ProtocolOperation => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_ProtocolProfile => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_ProtocolProfile => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile; + + @protected + ProtocolOperation + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); + } + @protected - ProtocolOperation - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolProfile + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( dynamic raw, ) { // Codec=Dco (DartCObject based), see doc to use other codecs - return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); + return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); } @protected @@ -1026,6 +4549,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); } + @protected + ProtocolProfile + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); + } + + @protected + ProtocolProfile + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); + } + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -1035,18 +4576,129 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ProtocolOperationImpl.frbInternalDcoDecode(raw as List); } + @protected + ProtocolProfile + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); + } + @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw as String; } + @protected + AdminAction dco_decode_admin_action(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AdminAction.values[raw as int]; + } + + @protected + AdminConfigurationPatch dco_decode_admin_configuration_patch(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 6) + throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + return AdminConfigurationPatch( + ledOn: dco_decode_opt_box_autoadd_bool(arr[0]), + ndefReadOnly: dco_decode_opt_box_autoadd_bool(arr[1]), + ndefEnabled: dco_decode_opt_box_autoadd_bool(arr[2]), + webusbLanding: dco_decode_opt_box_autoadd_bool(arr[3]), + featureMask: dco_decode_u_8(arr[4]), + featureValues: dco_decode_u_8(arr[5]), + ); + } + + @protected + AdminProgress dco_decode_admin_progress(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return AdminProgress( + confirmedWrites: dco_decode_u_32(arr[0]), + reprobeRequired: dco_decode_bool(arr[1]), + ); + } + + @protected + AdminReadOperation dco_decode_admin_read_operation(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AdminReadOperation.values[raw as int]; + } + + @protected + AdminResult dco_decode_admin_result(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return AdminResult( + kind: dco_decode_admin_value_kind(arr[0]), + data: dco_decode_list_prim_u_8_strict(arr[1]), + progress: dco_decode_admin_progress(arr[2]), + ); + } + + @protected + AdminValueKind dco_decode_admin_value_kind(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AdminValueKind.values[raw as int]; + } + @protected bool dco_decode_bool(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw as bool; } + @protected + BootstrapIdentityStep dco_decode_bootstrap_identity_step(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return BootstrapIdentityStep.values[raw as int]; + } + + @protected + ProtocolProfile + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + raw, + ); + } + + @protected + AdminConfigurationPatch dco_decode_box_autoadd_admin_configuration_patch( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_admin_configuration_patch(raw); + } + + @protected + AdminProgress dco_decode_box_autoadd_admin_progress(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_admin_progress(raw); + } + + @protected + AdminResult dco_decode_box_autoadd_admin_result(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_admin_result(raw); + } + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + @protected PivPrivateKeyData dco_decode_box_autoadd_piv_private_key_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1120,6 +4772,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_String(raw); } + @protected + ProtocolProfile? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + raw, + ); + } + + @protected + AdminProgress? dco_decode_opt_box_autoadd_admin_progress(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_admin_progress(raw); + } + + @protected + AdminResult? dco_decode_opt_box_autoadd_admin_result(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_admin_result(raw); + } + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_bool(raw); + } + @protected PivPrivateKeyData? dco_decode_opt_box_autoadd_piv_private_key_data( dynamic raw, @@ -1160,6 +4843,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); } + @protected + PivCredentialOperation dco_decode_piv_credential_operation(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PivCredentialOperation.values[raw as int]; + } + @protected PivImportFileData dco_decode_piv_import_file_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1222,12 +4911,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ProtocolStep dco_decode_protocol_step(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) - throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return ProtocolStep( command: dco_decode_opt_list_prim_u_8_strict(arr[0]), data: dco_decode_opt_list_prim_u_8_strict(arr[1]), error: dco_decode_opt_box_autoadd_protocol_error(arr[2]), + profile: + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + arr[3], + ), + admin: dco_decode_opt_box_autoadd_admin_result(arr[4]), ); } @@ -1257,104 +4951,266 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - int dco_decode_u_16(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; + int dco_decode_u_16(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_u_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + X509CertData dco_decode_x_509_cert_data(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 12) + throw Exception('unexpected arr length: expect 12 but see ${arr.length}'); + return X509CertData( + bytes: dco_decode_list_prim_u_8_strict(arr[0]), + subject: dco_decode_String(arr[1]), + issuer: dco_decode_String(arr[2]), + notBefore: dco_decode_String(arr[3]), + notAfter: dco_decode_String(arr[4]), + serialNumber: dco_decode_String(arr[5]), + signatureAlgorithm: dco_decode_String(arr[6]), + signatureValue: dco_decode_list_prim_u_8_strict(arr[7]), + publicKeyAlgorithm: dco_decode_String(arr[8]), + publicKeySize: dco_decode_usize(arr[9]), + subjectPublicKeyInfo: dco_decode_list_prim_u_8_strict(arr[10]), + rawPublicKey: dco_decode_list_prim_u_8_strict(arr[11]), + ); + } + + @protected + ProtocolOperation + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolProfile + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolOperation + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolProfile + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolProfile + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolOperation + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolProfile + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + AdminAction sse_decode_admin_action(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return AdminAction.values[inner]; + } + + @protected + AdminConfigurationPatch sse_decode_admin_configuration_patch( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_ledOn = sse_decode_opt_box_autoadd_bool(deserializer); + var var_ndefReadOnly = sse_decode_opt_box_autoadd_bool(deserializer); + var var_ndefEnabled = sse_decode_opt_box_autoadd_bool(deserializer); + var var_webusbLanding = sse_decode_opt_box_autoadd_bool(deserializer); + var var_featureMask = sse_decode_u_8(deserializer); + var var_featureValues = sse_decode_u_8(deserializer); + return AdminConfigurationPatch( + ledOn: var_ledOn, + ndefReadOnly: var_ndefReadOnly, + ndefEnabled: var_ndefEnabled, + webusbLanding: var_webusbLanding, + featureMask: var_featureMask, + featureValues: var_featureValues, + ); + } + + @protected + AdminProgress sse_decode_admin_progress(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_confirmedWrites = sse_decode_u_32(deserializer); + var var_reprobeRequired = sse_decode_bool(deserializer); + return AdminProgress( + confirmedWrites: var_confirmedWrites, + reprobeRequired: var_reprobeRequired, + ); } @protected - int dco_decode_u_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; + AdminReadOperation sse_decode_admin_read_operation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return AdminReadOperation.values[inner]; } @protected - int dco_decode_u_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; + AdminResult sse_decode_admin_result(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_kind = sse_decode_admin_value_kind(deserializer); + var var_data = sse_decode_list_prim_u_8_strict(deserializer); + var var_progress = sse_decode_admin_progress(deserializer); + return AdminResult(kind: var_kind, data: var_data, progress: var_progress); } @protected - void dco_decode_unit(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return; + AdminValueKind sse_decode_admin_value_kind(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return AdminValueKind.values[inner]; } @protected - BigInt dco_decode_usize(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dcoDecodeU64(raw); + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; } @protected - X509CertData dco_decode_x_509_cert_data(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 12) - throw Exception('unexpected arr length: expect 12 but see ${arr.length}'); - return X509CertData( - bytes: dco_decode_list_prim_u_8_strict(arr[0]), - subject: dco_decode_String(arr[1]), - issuer: dco_decode_String(arr[2]), - notBefore: dco_decode_String(arr[3]), - notAfter: dco_decode_String(arr[4]), - serialNumber: dco_decode_String(arr[5]), - signatureAlgorithm: dco_decode_String(arr[6]), - signatureValue: dco_decode_list_prim_u_8_strict(arr[7]), - publicKeyAlgorithm: dco_decode_String(arr[8]), - publicKeySize: dco_decode_usize(arr[9]), - subjectPublicKeyInfo: dco_decode_list_prim_u_8_strict(arr[10]), - rawPublicKey: dco_decode_list_prim_u_8_strict(arr[11]), - ); + BootstrapIdentityStep sse_decode_bootstrap_identity_step( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return BootstrapIdentityStep.values[inner]; } @protected - ProtocolOperation - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolProfile + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - return ProtocolOperationImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); + return (sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + deserializer, + )); } @protected - ProtocolOperation - sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + AdminConfigurationPatch sse_decode_box_autoadd_admin_configuration_patch( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - return ProtocolOperationImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); + return (sse_decode_admin_configuration_patch(deserializer)); } @protected - ProtocolOperation - sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + AdminProgress sse_decode_box_autoadd_admin_progress( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - return ProtocolOperationImpl.frbInternalSseDecode( - sse_decode_usize(deserializer), - sse_decode_i_32(deserializer), - ); + return (sse_decode_admin_progress(deserializer)); } @protected - String sse_decode_String(SseDeserializer deserializer) { + AdminResult sse_decode_box_autoadd_admin_result( + SseDeserializer deserializer, + ) { // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_list_prim_u_8_strict(deserializer); - return utf8.decoder.convert(inner); + return (sse_decode_admin_result(deserializer)); } @protected - bool sse_decode_bool(SseDeserializer deserializer) { + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getUint8() != 0; + return (sse_decode_bool(deserializer)); } @protected @@ -1451,6 +5307,59 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + ProtocolProfile? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + deserializer, + )); + } else { + return null; + } + } + + @protected + AdminProgress? sse_decode_opt_box_autoadd_admin_progress( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_admin_progress(deserializer)); + } else { + return null; + } + } + + @protected + AdminResult? sse_decode_opt_box_autoadd_admin_result( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_admin_result(deserializer)); + } else { + return null; + } + } + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_bool(deserializer)); + } else { + return null; + } + } + @protected PivPrivateKeyData? sse_decode_opt_box_autoadd_piv_private_key_data( SseDeserializer deserializer, @@ -1523,6 +5432,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + PivCredentialOperation sse_decode_piv_credential_operation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return PivCredentialOperation.values[inner]; + } + @protected PivImportFileData sse_decode_piv_import_file_data( SseDeserializer deserializer, @@ -1602,7 +5520,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_command = sse_decode_opt_list_prim_u_8_strict(deserializer); var var_data = sse_decode_opt_list_prim_u_8_strict(deserializer); var var_error = sse_decode_opt_box_autoadd_protocol_error(deserializer); - return ProtocolStep(command: var_command, data: var_data, error: var_error); + var var_profile = + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + deserializer, + ); + var var_admin = sse_decode_opt_box_autoadd_admin_result(deserializer); + return ProtocolStep( + command: var_command, + data: var_data, + error: var_error, + profile: var_profile, + admin: var_admin, + ); } @protected @@ -1706,53 +5635,213 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @protected void - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( - ProtocolOperation self, + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolProfileImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolProfileImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolProfileImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + ProtocolOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolOperationImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as ProtocolProfileImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_admin_action(AdminAction self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_admin_configuration_patch( + AdminConfigurationPatch self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_box_autoadd_bool(self.ledOn, serializer); + sse_encode_opt_box_autoadd_bool(self.ndefReadOnly, serializer); + sse_encode_opt_box_autoadd_bool(self.ndefEnabled, serializer); + sse_encode_opt_box_autoadd_bool(self.webusbLanding, serializer); + sse_encode_u_8(self.featureMask, serializer); + sse_encode_u_8(self.featureValues, serializer); + } + + @protected + void sse_encode_admin_progress(AdminProgress self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_32(self.confirmedWrites, serializer); + sse_encode_bool(self.reprobeRequired, serializer); + } + + @protected + void sse_encode_admin_read_operation( + AdminReadOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_admin_result(AdminResult self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_admin_value_kind(self.kind, serializer); + sse_encode_list_prim_u_8_strict(self.data, serializer); + sse_encode_admin_progress(self.progress, serializer); + } + + @protected + void sse_encode_admin_value_kind( + AdminValueKind self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } + + @protected + void sse_encode_bootstrap_identity_step( + BootstrapIdentityStep self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as ProtocolOperationImpl).frbInternalSseEncode(move: true), + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + self, serializer, ); } @protected - void - sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( - ProtocolOperation self, + void sse_encode_box_autoadd_admin_configuration_patch( + AdminConfigurationPatch self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as ProtocolOperationImpl).frbInternalSseEncode(move: false), - serializer, - ); + sse_encode_admin_configuration_patch(self, serializer); } @protected - void - sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( - ProtocolOperation self, + void sse_encode_box_autoadd_admin_progress( + AdminProgress self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_usize( - (self as ProtocolOperationImpl).frbInternalSseEncode(move: null), - serializer, - ); + sse_encode_admin_progress(self, serializer); } @protected - void sse_encode_String(String self, SseSerializer serializer) { + void sse_encode_box_autoadd_admin_result( + AdminResult self, + SseSerializer serializer, + ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + sse_encode_admin_result(self, serializer); } @protected - void sse_encode_bool(bool self, SseSerializer serializer) { + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putUint8(self ? 1 : 0); + sse_encode_bool(self, serializer); } @protected @@ -1856,6 +5945,59 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + self, + serializer, + ); + } + } + + @protected + void sse_encode_opt_box_autoadd_admin_progress( + AdminProgress? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_admin_progress(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_admin_result( + AdminResult? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_admin_result(self, serializer); + } + } + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_bool(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_piv_private_key_data( PivPrivateKeyData? self, @@ -1928,6 +6070,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_piv_credential_operation( + PivCredentialOperation self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_piv_import_file_data( PivImportFileData self, @@ -1987,6 +6138,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(self.command, serializer); sse_encode_opt_list_prim_u_8_strict(self.data, serializer); sse_encode_opt_box_autoadd_protocol_error(self.error, serializer); + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + self.profile, + serializer, + ); + sse_encode_opt_box_autoadd_admin_result(self.admin, serializer); } @protected @@ -2080,6 +6236,10 @@ class ProtocolOperationImpl extends RustOpaque implements ProtocolOperation { .rust_arc_decrement_strong_count_ProtocolOperationPtr, ); + /// Retained upstream progress, available even after protocol failure. + AdminProgress? adminProgress() => RustLib.instance.api + .crateApiProtocolProtocolOperationAdminProgress(that: this); + /// Consume one complete, unprocessed response including SW1/SW2. /// The input is copied into zeroizing storage and released before returning. ProtocolStep advance({required List response}) => RustLib.instance.api @@ -2093,3 +6253,752 @@ class ProtocolOperationImpl extends RustOpaque implements ProtocolOperation { ProtocolStep start() => RustLib.instance.api.crateApiProtocolProtocolOperationStart(that: this); } + +@sealed +class ProtocolProfileImpl extends RustOpaque implements ProtocolProfile { + // Not to be used by end users + ProtocolProfileImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + ProtocolProfileImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_ProtocolProfile, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_ProtocolProfile, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_ProtocolProfilePtr, + ); + + ProtocolOperation adminAction({ + required AdminAction kind, + Uint8List? pin, + required List data, + required int index, + required int value, + required bool existing, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileAdminAction( + that: this, + kind: kind, + pin: pin, + data: data, + index: index, + value: value, + existing: existing, + ); + + /// `existing` reuses the caller's authorized Admin transaction and forbids + /// a PIN; otherwise the operation SELECTs and verifies the supplied PIN. + ProtocolOperation adminConfigure({ + required AdminConfigurationPatch patch, + Uint8List? pin, + required bool existing, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileAdminConfigure( + that: this, + patch: patch, + pin: pin, + existing: existing, + ); + + /// Read both typed PASS slot configurations (INS 43). `existing` reuses the + /// caller's selected, already authorized Admin transaction and forbids a + /// PIN; otherwise the operation SELECTs and verifies the supplied PIN. + /// Secret slot material is never returned by firmware. + ProtocolOperation adminPassSlots({Uint8List? pin, required bool existing}) => + RustLib.instance.api.crateApiProtocolProtocolProfileAdminPassSlots( + that: this, + pin: pin, + existing: existing, + ); + + /// `existing` reuses the caller's selected, already authorized Admin + /// transaction and forbids a PIN; otherwise the operation SELECTs and + /// verifies only an explicitly supplied PIN. + ProtocolOperation adminRead({ + required AdminReadOperation kind, + Uint8List? pin, + required bool existing, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileAdminRead( + that: this, + kind: kind, + pin: pin, + existing: existing, + ); + + /// Replace one PASS slot (INS 44): slot 0 = short touch, 1 = long touch; + /// kind 0 = off, 1 = static password (data, append_enter), 2 = HMAC-SHA1 + /// (data = 20-byte key). OATH slots are configured through the OATH + /// applet, never here. The write is validated before any I/O. + ProtocolOperation adminSetPassSlot({ + required int slot, + required int kind, + required List data, + required bool appendEnter, + Uint8List? pin, + required bool existing, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileAdminSetPassSlot( + that: this, + slot: slot, + kind: kind, + data: data, + appendEnter: appendEnter, + pin: pin, + existing: existing, + ); + + /// Idempotent local cleanup. Does not alter the card or already-created ops. + void close() => + RustLib.instance.api.crateApiProtocolProtocolProfileClose(that: this); + + /// Bootstrap observations, not an authenticated session or cached credential. + Uint8List? firmware() => + RustLib.instance.api.crateApiProtocolProtocolProfileFirmware(that: this); + + String? model() => + RustLib.instance.api.crateApiProtocolProtocolProfileModel(that: this); + + ProtocolOperation oathCalculate({ + required List name, + required int kind, + required int algorithm, + Uint8List? challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathCalculate( + that: this, + name: name, + kind: kind, + algorithm: algorithm, + challenge: challenge, + format: format, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathCalculateAll({ + required List challenge, + required int format, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathCalculateAll( + that: this, + challenge: challenge, + format: format, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathClearCode({Uint8List? key, Uint8List? challenge}) => + RustLib.instance.api.crateApiProtocolProtocolProfileOathClearCode( + that: this, + key: key, + challenge: challenge, + ); + + ProtocolOperation oathDelete({ + required List name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathDelete( + that: this, + name: name, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathList({ + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathList( + that: this, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathPut({ + required List name, + required int kind, + required int algorithm, + required int digits, + required List secret, + required bool requireTouch, + required bool increasing, + required int initialCounter, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathPut( + that: this, + name: name, + kind: kind, + algorithm: algorithm, + digits: digits, + secret: secret, + requireTouch: requireTouch, + increasing: increasing, + initialCounter: initialCounter, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathRename({ + required List old, + required List new_, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathRename( + that: this, + old: old, + new_: new_, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathSelect() => RustLib.instance.api + .crateApiProtocolProtocolProfileOathSelect(that: this); + + /// The current key validates in the same operation only when supplied; + /// setting the first code on an unprotected applet passes no old key. + ProtocolOperation oathSetCode({ + Uint8List? oldKey, + required List newKey, + required List challenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathSetCode( + that: this, + oldKey: oldKey, + newKey: newKey, + challenge: challenge, + ); + + /// Mark an existing HOTP credential as the touch keyboard-emulation + /// default: slot 0 = short, 1 = long. The legacy (pre-3.0) single-slot + /// dialect is chosen from the probed profile; a long slot or append-enter + /// there fails construction with InvalidArgument before any I/O. + ProtocolOperation oathSetDefault({ + required int slot, + required bool appendEnter, + required String name, + Uint8List? accessKey, + Uint8List? accessChallenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathSetDefault( + that: this, + slot: slot, + appendEnter: appendEnter, + name: name, + accessKey: accessKey, + accessChallenge: accessChallenge, + ); + + ProtocolOperation oathValidate({ + required List key, + required List challenge, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOathValidate( + that: this, + key: key, + challenge: challenge, + ); + + ProtocolOperation openpgpActivate({required List password}) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpActivate( + that: this, + password: password, + ); + + ProtocolOperation openpgpChangePassword({ + required int reference, + required List old, + required List new_, + }) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpChangePassword( + that: this, + reference: reference, + old: old, + new_: new_, + ); + + ProtocolOperation openpgpGenerateKey({ + required int slot, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpGenerateKey( + that: this, + slot: slot, + password: password, + ); + + ProtocolOperation openpgpLogout({required int reference}) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpLogout( + that: this, + reference: reference, + ); + + ProtocolOperation openpgpPinStatus({required int reference}) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpPinStatus( + that: this, + reference: reference, + ); + + ProtocolOperation openpgpReadCertificate({required int slot}) => RustLib + .instance + .api + .crateApiProtocolProtocolProfileOpenpgpReadCertificate( + that: this, + slot: slot, + ); + + ProtocolOperation openpgpReadData({required int tag}) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpReadData(that: this, tag: tag); + + /// Set PW1/reset-code/PW3 retry limits after explicit PW3 verification. + /// The card resets PW1/PW3 to firmware defaults and clears authorization; + /// the retry-reset capability gates construction before any I/O. + ProtocolOperation openpgpResetRetries({ + required List retries, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpResetRetries( + that: this, + retries: retries, + password: password, + ); + + ProtocolOperation openpgpTerminate({required List password}) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpTerminate( + that: this, + password: password, + ); + + /// Reset PW1 after explicit PW3 verification; does not change PW3. + ProtocolOperation openpgpUnblockWithAdmin({ + required List newPin, + required List password, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpUnblockWithAdmin( + that: this, + newPin: newPin, + password: password, + ); + + /// Reset PW1 using reset-code||new-PW1; no password verification is inserted. + ProtocolOperation openpgpUnblockWithCode({ + required List code, + required List newPin, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpUnblockWithCode( + that: this, + code: code, + newPin: newPin, + ); + + ProtocolOperation openpgpVerify({ + required int reference, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpVerify( + that: this, + reference: reference, + password: password, + ); + + ProtocolOperation openpgpWriteCertificate({ + required int slot, + required List certificate, + required List password, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpWriteCertificate( + that: this, + slot: slot, + certificate: certificate, + password: password, + ); + + ProtocolOperation openpgpWriteLanguage({ + required List language, + required List password, + }) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteLanguage( + that: this, + language: language, + password: password, + ); + + ProtocolOperation openpgpWriteLogin({ + required List login, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteLogin( + that: this, + login: login, + password: password, + ); + + ProtocolOperation openpgpWriteName({ + required List name, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteName( + that: this, + name: name, + password: password, + ); + + /// Set (Some) or clear (None) the reset code after explicit PW3 verification. + ProtocolOperation openpgpWriteResetCode({ + Uint8List? resetCode, + required List password, + }) => + RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteResetCode( + that: this, + resetCode: resetCode, + password: password, + ); + + ProtocolOperation openpgpWriteSex({ + required int sex, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteSex( + that: this, + sex: sex, + password: password, + ); + + /// Whether one explicit PW1-sign verification may authorize multiple + /// signatures (PW status bytes policy). Written after PW3 verification. + ProtocolOperation openpgpWriteSignaturePinPolicy({ + required bool reuse, + required List password, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpWriteSignaturePinPolicy( + that: this, + reuse: reuse, + password: password, + ); + + /// Touch cache duration in seconds after PW3 verification; UIF-gated. + ProtocolOperation openpgpWriteTouchCacheTime({ + required int seconds, + required List password, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpWriteTouchCacheTime( + that: this, + seconds: seconds, + password: password, + ); + + /// Slot touch policy (0 off, 1 on, 2 permanent) after PW3 verification. + /// Emits the standard two-byte UIF field; the UIF capability gates it. + ProtocolOperation openpgpWriteTouchPolicy({ + required int slot, + required int policy, + required List password, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfileOpenpgpWriteTouchPolicy( + that: this, + slot: slot, + policy: policy, + password: password, + ); + + ProtocolOperation openpgpWriteUrl({ + required List url, + required List password, + }) => RustLib.instance.api.crateApiProtocolProtocolProfileOpenpgpWriteUrl( + that: this, + url: url, + password: password, + ); + + /// Resolve an observed key algorithm to Console's fixed display enum IDs. + /// These are NOT device wire IDs and never authorize a key operation. + /// Unknown algorithms and closed profiles return None. + int? pivAlgorithmDisplayId({required int wireId}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivAlgorithmDisplayId( + that: this, + wireId: wireId, + ); + + /// INS F9 attestation certificate for a generated slot. The selected-context + /// factory asserts no credential and consumes no authentication state. + ProtocolOperation pivAttest({required int slot}) => RustLib.instance.api + .crateApiProtocolProtocolProfilePivAttest(that: this, slot: slot); + + /// Preserve Console's explicit external authentication mode. libcanokey owns + /// the full challenge exchange and cryptography; no host login is inferred. + ProtocolOperation pivAuthenticateManagement({ + required int algorithm, + required List key, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfilePivAuthenticateManagement( + that: this, + algorithm: algorithm, + key: key, + ); + + /// Reuse the caller's selected transaction; the profile is evidence only. + ProtocolOperation pivCertificate({required int objectId}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivCertificate( + that: this, + objectId: objectId, + ); + + /// Credentials are copied into upstream zeroizing owners before returning. + /// Success does not turn this immutable profile into an authorization token. + ProtocolOperation pivCredential({ + required PivCredentialOperation kind, + required List current, + required List replacement, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivCredential( + that: this, + kind: kind, + current: current, + replacement: replacement, + ); + + ProtocolOperation pivDecapsulate({ + required int slot, + required List ciphertext, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivDecapsulate( + that: this, + slot: slot, + ciphertext: ciphertext, + ); + + ProtocolOperation pivDecrypt({ + required int slot, + required int algorithm, + required List ciphertext, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivDecrypt( + that: this, + slot: slot, + algorithm: algorithm, + ciphertext: ciphertext, + ); + + ProtocolOperation pivDeleteCertificate({required int objectId}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivDeleteCertificate( + that: this, + objectId: objectId, + ); + + ProtocolOperation pivDeleteKey({required int slot}) => RustLib.instance.api + .crateApiProtocolProtocolProfilePivDeleteKey(that: this, slot: slot); + + ProtocolOperation pivDerive({ + required int slot, + required int algorithm, + required List peer, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivDerive( + that: this, + slot: slot, + algorithm: algorithm, + peer: peer, + ); + + ProtocolOperation pivGenerateKey({ + required int slot, + required int algorithm, + required int pinPolicy, + required int touchPolicy, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivGenerateKey( + that: this, + slot: slot, + algorithm: algorithm, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + ); + + ProtocolOperation pivImportEcKey({ + required int slot, + required int algorithm, + required List scalar, + required int pinPolicy, + required int touchPolicy, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivImportEcKey( + that: this, + slot: slot, + algorithm: algorithm, + scalar: scalar, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + ); + + ProtocolOperation pivImportEd25519Key({ + required int slot, + required List seed, + required int pinPolicy, + required int touchPolicy, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivImportEd25519Key( + that: this, + slot: slot, + seed: seed, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + ); + + /// Import a post-quantum seed (INS FE): kind 0 = ML-DSA-65 (32-byte seed), + /// 1 = ML-KEM-768 (64-byte d||z seed). Semantics match the EC/RSA/Ed25519 + /// imports: Access::Existing inside the caller's reserved transaction. + ProtocolOperation pivImportPqSeed({ + required int slot, + required int kind, + required List seed, + required int pinPolicy, + required int touchPolicy, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivImportPqSeed( + that: this, + slot: slot, + kind: kind, + seed: seed, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + ); + + ProtocolOperation pivImportRsaKey({ + required int slot, + required int algorithm, + required List p, + required List q, + required List dp, + required List dq, + required List qinv, + required int pinPolicy, + required int touchPolicy, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivImportRsaKey( + that: this, + slot: slot, + algorithm: algorithm, + p: p, + q: q, + dp: dp, + dq: dq, + qinv: qinv, + pinPolicy: pinPolicy, + touchPolicy: touchPolicy, + ); + + /// Read metadata in the caller's already selected PIV session. No probing, + /// SELECT or authentication is inserted, including after VERIFY. + ProtocolOperation pivMetadata({required int reference}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivMetadata( + that: this, + reference: reference, + ); + + /// Read the PIV metadata directory in the caller's selected transaction. + /// The upstream parser validates framing and preserves the raw directory. + ProtocolOperation pivMetadataDirectory() => RustLib.instance.api + .crateApiProtocolProtocolProfilePivMetadataDirectory(that: this); + + ProtocolOperation pivMoveKey({required int source, required int target}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivMoveKey( + that: this, + source: source, + target: target, + ); + + /// Return the normalized object value. libcanokey owns framing and parsing. + ProtocolOperation pivReadObject({required List objectId}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivReadObject( + that: this, + objectId: objectId, + ); + + ProtocolOperation pivResetPinPukRetries({ + required int pinRetries, + required int pukRetries, + }) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivResetPinPukRetries( + that: this, + pinRetries: pinRetries, + pukRetries: pukRetries, + ); + + ProtocolOperation pivSetAlgorithmConfig({required List raw}) => + RustLib.instance.api.crateApiProtocolProtocolProfilePivSetAlgorithmConfig( + that: this, + raw: raw, + ); + + ProtocolOperation pivSetContainerName({ + required int slot, + required String name, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivSetContainerName( + that: this, + slot: slot, + name: name, + ); + + ProtocolOperation pivSetManagementKey({ + required int algorithm, + required List key, + required int touch, + required bool updateProtected, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivSetManagementKey( + that: this, + algorithm: algorithm, + key: key, + touch: touch, + updateProtected: updateProtected, + ); + + ProtocolOperation pivSign({ + required int slot, + required int algorithm, + required List input, + required int inputKind, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivSign( + that: this, + slot: slot, + algorithm: algorithm, + input: input, + inputKind: inputKind, + ); + + /// Firmware streaming signature modes: 0 = ML-DSA-65 with the empty context, + /// 1 = randomized Ed25519 (wire mode FF), 2 = SM2 full message with an + /// optional user ID. Classic digest/padded signing stays with `piv_sign`. + ProtocolOperation pivSignStreaming({ + required int slot, + required int mode, + required List message, + Uint8List? userId, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivSignStreaming( + that: this, + slot: slot, + mode: mode, + message: message, + userId: userId, + ); + + ProtocolOperation pivSm2Agreement({ + required int slot, + required int role, + required List peerStatic, + required List peerEphemeral, + Uint8List? userId, + Uint8List? peerId, + required int keyLen, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivSm2Agreement( + that: this, + slot: slot, + role: role, + peerStatic: peerStatic, + peerEphemeral: peerEphemeral, + userId: userId, + peerId: peerId, + keyLen: keyLen, + ); + + /// Writes may be partially applied before failure; never retry or roll back. + /// Existing delegates authorization to the card in the caller-owned lease. + ProtocolOperation pivWriteObject({ + required List objectId, + required List data, + }) => RustLib.instance.api.crateApiProtocolProtocolProfilePivWriteObject( + that: this, + objectId: objectId, + data: data, + ); + + /// The serial observed during explicit discovery. Missing observations stay absent. + Uint8List? serial() => + RustLib.instance.api.crateApiProtocolProtocolProfileSerial(that: this); +} diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index c2fe3b9..022a3a4 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -25,30 +25,99 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr; + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolProfilePtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr; + @protected ProtocolOperation dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + ProtocolProfile + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); + @protected + AdminAction dco_decode_admin_action(dynamic raw); + + @protected + AdminConfigurationPatch dco_decode_admin_configuration_patch(dynamic raw); + + @protected + AdminProgress dco_decode_admin_progress(dynamic raw); + + @protected + AdminReadOperation dco_decode_admin_read_operation(dynamic raw); + + @protected + AdminResult dco_decode_admin_result(dynamic raw); + + @protected + AdminValueKind dco_decode_admin_value_kind(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); + @protected + BootstrapIdentityStep dco_decode_bootstrap_identity_step(dynamic raw); + + @protected + ProtocolProfile + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + AdminConfigurationPatch dco_decode_box_autoadd_admin_configuration_patch( + dynamic raw, + ); + + @protected + AdminProgress dco_decode_box_autoadd_admin_progress(dynamic raw); + + @protected + AdminResult dco_decode_box_autoadd_admin_result(dynamic raw); + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw); + @protected PivPrivateKeyData dco_decode_box_autoadd_piv_private_key_data(dynamic raw); @@ -86,6 +155,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + ProtocolProfile? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + AdminProgress? dco_decode_opt_box_autoadd_admin_progress(dynamic raw); + + @protected + AdminResult? dco_decode_opt_box_autoadd_admin_result(dynamic raw); + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw); + @protected PivPrivateKeyData? dco_decode_opt_box_autoadd_piv_private_key_data( dynamic raw, @@ -106,6 +190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + @protected + PivCredentialOperation dco_decode_piv_credential_operation(dynamic raw); + @protected PivImportFileData dco_decode_piv_import_file_data(dynamic raw); @@ -153,24 +240,97 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + ProtocolProfile + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); + @protected + AdminAction sse_decode_admin_action(SseDeserializer deserializer); + + @protected + AdminConfigurationPatch sse_decode_admin_configuration_patch( + SseDeserializer deserializer, + ); + + @protected + AdminProgress sse_decode_admin_progress(SseDeserializer deserializer); + + @protected + AdminReadOperation sse_decode_admin_read_operation( + SseDeserializer deserializer, + ); + + @protected + AdminResult sse_decode_admin_result(SseDeserializer deserializer); + + @protected + AdminValueKind sse_decode_admin_value_kind(SseDeserializer deserializer); + @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + BootstrapIdentityStep sse_decode_bootstrap_identity_step( + SseDeserializer deserializer, + ); + + @protected + ProtocolProfile + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + AdminConfigurationPatch sse_decode_box_autoadd_admin_configuration_patch( + SseDeserializer deserializer, + ); + + @protected + AdminProgress sse_decode_box_autoadd_admin_progress( + SseDeserializer deserializer, + ); + + @protected + AdminResult sse_decode_box_autoadd_admin_result(SseDeserializer deserializer); + + @protected + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer); + @protected PivPrivateKeyData sse_decode_box_autoadd_piv_private_key_data( SseDeserializer deserializer, @@ -216,6 +376,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + ProtocolProfile? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + AdminProgress? sse_decode_opt_box_autoadd_admin_progress( + SseDeserializer deserializer, + ); + + @protected + AdminResult? sse_decode_opt_box_autoadd_admin_result( + SseDeserializer deserializer, + ); + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer); + @protected PivPrivateKeyData? sse_decode_opt_box_autoadd_piv_private_key_data( SseDeserializer deserializer, @@ -240,6 +419,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + PivCredentialOperation sse_decode_piv_credential_operation( + SseDeserializer deserializer, + ); + @protected PivImportFileData sse_decode_piv_import_file_data( SseDeserializer deserializer, @@ -292,6 +476,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -299,6 +490,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -306,12 +511,80 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); + @protected + void sse_encode_admin_action(AdminAction self, SseSerializer serializer); + + @protected + void sse_encode_admin_configuration_patch( + AdminConfigurationPatch self, + SseSerializer serializer, + ); + + @protected + void sse_encode_admin_progress(AdminProgress self, SseSerializer serializer); + + @protected + void sse_encode_admin_read_operation( + AdminReadOperation self, + SseSerializer serializer, + ); + + @protected + void sse_encode_admin_result(AdminResult self, SseSerializer serializer); + + @protected + void sse_encode_admin_value_kind( + AdminValueKind self, + SseSerializer serializer, + ); + @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_bootstrap_identity_step( + BootstrapIdentityStep self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_configuration_patch( + AdminConfigurationPatch self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_progress( + AdminProgress self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_result( + AdminResult self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_piv_private_key_data( PivPrivateKeyData self, @@ -363,6 +636,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_admin_progress( + AdminProgress? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_admin_result( + AdminResult? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_piv_private_key_data( PivPrivateKeyData? self, @@ -393,6 +688,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_piv_credential_operation( + PivCredentialOperation self, + SseSerializer serializer, + ); + @protected void sse_encode_piv_import_file_data( PivImportFileData self, @@ -495,4 +796,38 @@ class RustLibWire implements BaseWire { late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation = _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr + .asFunction)>(); } diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index b0a1376..bee6c93 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -27,30 +27,99 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_ProtocolProfilePtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile; + @protected ProtocolOperation dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + ProtocolProfile + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( dynamic raw, ); + @protected + ProtocolProfile + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); + @protected + AdminAction dco_decode_admin_action(dynamic raw); + + @protected + AdminConfigurationPatch dco_decode_admin_configuration_patch(dynamic raw); + + @protected + AdminProgress dco_decode_admin_progress(dynamic raw); + + @protected + AdminReadOperation dco_decode_admin_read_operation(dynamic raw); + + @protected + AdminResult dco_decode_admin_result(dynamic raw); + + @protected + AdminValueKind dco_decode_admin_value_kind(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); + @protected + BootstrapIdentityStep dco_decode_bootstrap_identity_step(dynamic raw); + + @protected + ProtocolProfile + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + AdminConfigurationPatch dco_decode_box_autoadd_admin_configuration_patch( + dynamic raw, + ); + + @protected + AdminProgress dco_decode_box_autoadd_admin_progress(dynamic raw); + + @protected + AdminResult dco_decode_box_autoadd_admin_result(dynamic raw); + + @protected + bool dco_decode_box_autoadd_bool(dynamic raw); + @protected PivPrivateKeyData dco_decode_box_autoadd_piv_private_key_data(dynamic raw); @@ -88,6 +157,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + ProtocolProfile? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + dynamic raw, + ); + + @protected + AdminProgress? dco_decode_opt_box_autoadd_admin_progress(dynamic raw); + + @protected + AdminResult? dco_decode_opt_box_autoadd_admin_result(dynamic raw); + + @protected + bool? dco_decode_opt_box_autoadd_bool(dynamic raw); + @protected PivPrivateKeyData? dco_decode_opt_box_autoadd_piv_private_key_data( dynamic raw, @@ -108,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + @protected + PivCredentialOperation dco_decode_piv_credential_operation(dynamic raw); + @protected PivImportFileData dco_decode_piv_import_file_data(dynamic raw); @@ -155,24 +242,97 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + ProtocolProfile + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( SseDeserializer deserializer, ); + @protected + ProtocolProfile + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); + @protected + AdminAction sse_decode_admin_action(SseDeserializer deserializer); + + @protected + AdminConfigurationPatch sse_decode_admin_configuration_patch( + SseDeserializer deserializer, + ); + + @protected + AdminProgress sse_decode_admin_progress(SseDeserializer deserializer); + + @protected + AdminReadOperation sse_decode_admin_read_operation( + SseDeserializer deserializer, + ); + + @protected + AdminResult sse_decode_admin_result(SseDeserializer deserializer); + + @protected + AdminValueKind sse_decode_admin_value_kind(SseDeserializer deserializer); + @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + BootstrapIdentityStep sse_decode_bootstrap_identity_step( + SseDeserializer deserializer, + ); + + @protected + ProtocolProfile + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + AdminConfigurationPatch sse_decode_box_autoadd_admin_configuration_patch( + SseDeserializer deserializer, + ); + + @protected + AdminProgress sse_decode_box_autoadd_admin_progress( + SseDeserializer deserializer, + ); + + @protected + AdminResult sse_decode_box_autoadd_admin_result(SseDeserializer deserializer); + + @protected + bool sse_decode_box_autoadd_bool(SseDeserializer deserializer); + @protected PivPrivateKeyData sse_decode_box_autoadd_piv_private_key_data( SseDeserializer deserializer, @@ -218,6 +378,25 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + ProtocolProfile? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ); + + @protected + AdminProgress? sse_decode_opt_box_autoadd_admin_progress( + SseDeserializer deserializer, + ); + + @protected + AdminResult? sse_decode_opt_box_autoadd_admin_result( + SseDeserializer deserializer, + ); + + @protected + bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer); + @protected PivPrivateKeyData? sse_decode_opt_box_autoadd_piv_private_key_data( SseDeserializer deserializer, @@ -242,6 +421,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + PivCredentialOperation sse_decode_piv_credential_operation( + SseDeserializer deserializer, + ); + @protected PivImportFileData sse_decode_piv_import_file_data( SseDeserializer deserializer, @@ -294,6 +478,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -301,6 +492,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -308,12 +513,80 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); + @protected + void sse_encode_admin_action(AdminAction self, SseSerializer serializer); + + @protected + void sse_encode_admin_configuration_patch( + AdminConfigurationPatch self, + SseSerializer serializer, + ); + + @protected + void sse_encode_admin_progress(AdminProgress self, SseSerializer serializer); + + @protected + void sse_encode_admin_read_operation( + AdminReadOperation self, + SseSerializer serializer, + ); + + @protected + void sse_encode_admin_result(AdminResult self, SseSerializer serializer); + + @protected + void sse_encode_admin_value_kind( + AdminValueKind self, + SseSerializer serializer, + ); + @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_bootstrap_identity_step( + BootstrapIdentityStep self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_configuration_patch( + AdminConfigurationPatch self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_progress( + AdminProgress self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_admin_result( + AdminResult self, + SseSerializer serializer, + ); + + @protected + void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_piv_private_key_data( PivPrivateKeyData self, @@ -365,6 +638,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ProtocolProfile? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_admin_progress( + AdminProgress? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_admin_result( + AdminResult? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_piv_private_key_data( PivPrivateKeyData? self, @@ -395,6 +690,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_piv_credential_operation( + PivCredentialOperation self, + SseSerializer serializer, + ); + @protected void sse_encode_piv_import_file_data( PivImportFileData self, @@ -470,6 +771,22 @@ class RustLibWire implements BaseWire { .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( ptr, ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr, + ); } @JS('wasm_bindgen') @@ -487,4 +804,14 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject { rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( int ptr, ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + int ptr, + ); } diff --git a/lib/views/applets/pass/dialogs/slot_config_dialog.dart b/lib/views/applets/pass/dialogs/slot_config_dialog.dart index bbb5f9f..5e84a7e 100644 --- a/lib/views/applets/pass/dialogs/slot_config_dialog.dart +++ b/lib/views/applets/pass/dialogs/slot_config_dialog.dart @@ -125,6 +125,8 @@ class _SlotConfigDialogState extends BaseDialogState return S.of(context).passSlotStatic; case PassSlotType.hmacSha1: return S.of(context).passSlotHmacSha1; + case PassSlotType.unknown: + return S.of(context).settingsKeyboardLayoutUnknown; } } diff --git a/lib/views/applets/pass/widgets/slot_card.dart b/lib/views/applets/pass/widgets/slot_card.dart index 2203306..0640891 100644 --- a/lib/views/applets/pass/widgets/slot_card.dart +++ b/lib/views/applets/pass/widgets/slot_card.dart @@ -123,5 +123,6 @@ class SlotCard extends StatelessWidget { PassSlotType.oath => '${S.of(context).passSlotHotp} (${slot.name})', PassSlotType.static => S.of(context).passSlotStatic, PassSlotType.hmacSha1 => S.of(context).passSlotHmacSha1, + PassSlotType.unknown => S.of(context).settingsKeyboardLayoutUnknown, }; } diff --git a/lib/views/applets/settings/settings_page.dart b/lib/views/applets/settings/settings_page.dart index a13f590..ab3e92b 100644 --- a/lib/views/applets/settings/settings_page.dart +++ b/lib/views/applets/settings/settings_page.dart @@ -6,6 +6,7 @@ import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/localization/hints.dart'; import 'package:canokey_console/helper/widgets/customized_text.dart'; import 'package:canokey_console/models/canokey.dart'; +import 'package:canokey_console/helper/utils/screenshot_mode.dart'; import 'package:canokey_console/views/applets/settings/widgets/action_card.dart'; import 'package:canokey_console/views/applets/settings/widgets/info_card.dart'; import 'package:canokey_console/views/applets/settings/widgets/other_settings_card.dart'; @@ -26,6 +27,27 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State { final _controller = Get.put(SettingsController()); + @override + void initState() { + super.initState(); + if (isWeb() && !ScreenshotMode.enabled) { + // onReady runs once per controller, not once per page entry. A retained + // controller must still read the current card when this page is mounted. + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + try { + await _controller.refreshData(); + } catch (error, stack) { + _controller.log.w( + 'Failed to refresh settings on page entry', + error: error, + stackTrace: stack, + ); + } + }); + } + } + @override Widget build(BuildContext context) { final mobile = ScreenMedia.getTypeFromWidth( diff --git a/lib/views/applets/webauthn/dialogs/force_pin_change_dialog.dart b/lib/views/applets/webauthn/dialogs/force_pin_change_dialog.dart index af7faa9..3e4d0c2 100644 --- a/lib/views/applets/webauthn/dialogs/force_pin_change_dialog.dart +++ b/lib/views/applets/webauthn/dialogs/force_pin_change_dialog.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/widgets/app_dialog.dart'; import 'package:canokey_console/helper/theme/admin_theme.dart'; @@ -13,12 +15,14 @@ class ForcePinChangeDialog extends BaseDialog { final Future Function(String currentPin, String newPin, bool savePin) onSubmit; final VoidCallback onCancel; + final Future Function() onFocus; const ForcePinChangeDialog({ super.key, required this.minPinLength, required this.onSubmit, required this.onCancel, + this.onFocus = SmartCard.eject, }); static Future show({ @@ -31,11 +35,14 @@ class ForcePinChangeDialog extends BaseDialog { onSubmit, required VoidCallback onCancel, }) { + final zone = Zone.current; return AppDialog.show( ForcePinChangeDialog( minPinLength: minPinLength, - onSubmit: onSubmit, - onCancel: onCancel, + onSubmit: (currentPin, newPin, savePin) => + zone.run(() => onSubmit(currentPin, newPin, savePin)), + onCancel: zone.bindCallback(onCancel), + onFocus: zone.bindCallback(SmartCard.eject), ), ); } @@ -214,7 +221,7 @@ class _ForcePinChangeDialogState extends BaseDialogState { autofocus: controller == _currentPin, obscureText: obscureText, validator: (value) => _validatePin(value, minLength: minLength), - onTap: SmartCard.eject, + onTap: widget.onFocus, onFieldSubmitted: onFieldSubmitted, decoration: InputDecoration( labelText: label, diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a579298..dd325dd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,10 +355,12 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-admin", "canokey-compat", + "canokey-ctap", + "canokey-ndef", "canokey-oath", "canokey-openpgp", "canokey-piv", @@ -368,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -377,7 +379,15 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +dependencies = [ + "canokey-protocol", +] + +[[package]] +name = "canokey-ctap" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-protocol", ] @@ -385,7 +395,7 @@ dependencies = [ [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -394,10 +404,18 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "canokey-ndef" +version = "0.1.0" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +dependencies = [ + "canokey-protocol", +] + [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -410,7 +428,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "canokey-compat", "canokey-key", @@ -420,7 +438,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -441,7 +459,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=10808e1760bf5efa86b5ff2e0b4acbeaba441edc#10808e1760bf5efa86b5ff2e0b4acbeaba441edc" +source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" dependencies = [ "thiserror 2.0.20", "zeroize", @@ -2672,6 +2690,7 @@ version = "0.1.0" dependencies = [ "aes 0.9.3", "canokey", + "canokey-protocol", "const-oid 0.10.2", "der 0.8.1", "des 0.9.0", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0c51620..9f097d4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "10808e1760bf5efa86b5ff2e0b4acbeaba441edc" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "95e1930ea86eb248d0295bce9880cee5c65fd85e" } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "95e1930ea86eb248d0295bce9880cee5c65fd85e" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/rust/src/api/decode.rs b/rust/src/api/decode.rs index d193f93..84cfeb2 100644 --- a/rust/src/api/decode.rs +++ b/rust/src/api/decode.rs @@ -6,8 +6,9 @@ use rxing::ImmutableReader; static LAZY_STATIC_QR_READER: Lazy = Lazy::new(QRCodeReader::default); fn rgba_to_argb(rgba: &[u8]) -> Vec { - let mut argb = Vec::with_capacity(rgba.len() / 4); - for pixel in rgba.chunks_exact(4) { + let (pixels, _) = rgba.as_chunks::<4>(); + let mut argb = Vec::with_capacity(pixels.len()); + for pixel in pixels { argb.push( ((pixel[3] as u32) << 24) | ((pixel[2] as u32) << 16) diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs index 4c0b955..e0d0a36 100644 --- a/rust/src/api/protocol.rs +++ b/rust/src/api/protocol.rs @@ -1,7 +1,11 @@ //! Owned libcanokey operations. Dart owns the selected connection and every I/O. use std::sync::Mutex; -use canokey::{piv, Error, ErrorKind, Operation, OperationOptions, SecretBytes, Step}; +use canokey::{ + admin, ctap, ndef, oath, openpgp, piv, Error, ErrorKind, Operation, OperationOptions, Phase, + SecretBytes, Step, +}; +use canokey_protocol::operation::{conversation, ResponseData}; /// Initial PIV operations; reads require an already selected PIV application. #[derive(Clone, Copy)] @@ -9,6 +13,180 @@ pub enum PivReadOperation { Select, Version, AlgorithmConfiguration, + PinStatus, +} + +/// Explicit actions in an already prepared lease; never SELECT or probe. +#[derive(Clone, Copy)] +pub enum PivCredentialOperation { + VerifyPin, + ChangePin, + ChangePuk, + UnblockPin, + Logout, +} + +/// Connection-bootstrap identity steps; raw Admin commands without a profile. +/// Upstream builders own encoding and safe Le correction; no authentication, +/// no other applet selection and no device observations are created. +#[derive(Clone, Copy)] +pub enum BootstrapIdentityStep { + SelectAdmin, + Serial, +} + +/// Admin operations always SELECT, then verify only an explicitly supplied PIN. +#[derive(Clone, Copy)] +pub enum AdminReadOperation { + Firmware, + Model, + Serial, + ChipId, + CoreCommit, + Configuration, + FlashUsage, + AppletUsage, + KeyboardLayout, + KeyboardKeymap, + NfcStatus, + Sm2Configuration, +} +#[derive(Clone, Copy)] +pub enum AdminAction { + VerifyPin, + ChangePin, + KeyboardInterface, + KeyboardReturn, + KeyboardKeymap, + ClearKeyboardKeymap, + LegacyPivExtensions, + LegacyTouch, + WriteSm2, + Nfc, + ResetOpenPgp, + ResetPiv, + ResetOath, + ResetNdef, + ResetCtap, + ResetPass, + FactoryReset, +} +pub struct AdminConfigurationPatch { + pub led_on: Option, + pub ndef_read_only: Option, + pub ndef_enabled: Option, + pub webusb_landing: Option, + pub feature_mask: u8, + pub feature_values: u8, +} +#[derive(Clone, Copy)] +pub enum AdminValueKind { + None, + Bytes, + Configuration, + LegacyConfiguration, + FlashUsage, + AppletUsage, + NfcStatus, + Sm2Configuration, + LegacySm2Configuration, + PinStatus, + KeyboardLayout, + KeyboardKeymap, + PassSlots, +} +/// Copy progress before close, including on transport failure or cancellation. +pub struct AdminProgress { + pub confirmed_writes: u32, + pub reprobe_required: bool, +} +pub struct AdminResult { + pub kind: AdminValueKind, + pub data: Vec, + pub progress: AdminProgress, +} +fn admin_progress(outcome: &admin::Outcome) -> AdminProgress { + AdminProgress { + confirmed_writes: outcome.confirmed_writes as u32, + reprobe_required: outcome.reprobe_required, + } +} +fn admin_result(outcome: admin::Outcome) -> AdminResult { + let progress = admin_progress(&outcome); + use admin::Value; + let (kind, data) = match outcome.value { + Value::None => (AdminValueKind::None, vec![]), + Value::Bytes(data) => (AdminValueKind::Bytes, data), + Value::Configuration(c) => (AdminValueKind::Configuration, c.raw().to_vec()), + Value::LegacyConfiguration(c) => (AdminValueKind::LegacyConfiguration, c.raw().to_vec()), + Value::FlashUsage(v) => (AdminValueKind::FlashUsage, vec![v.used_kib, v.total_kib]), + Value::AppletUsage(values) => ( + AdminValueKind::AppletUsage, + values + .into_iter() + .flat_map(|v| { + let mut data = vec![v.applet_id, v.flags]; + data.extend(v.logical_bytes.to_be_bytes()); + data + }) + .collect(), + ), + Value::NfcStatus(on) => (AdminValueKind::NfcStatus, vec![u8::from(on)]), + Value::Sm2Configuration(c) => (AdminValueKind::Sm2Configuration, c.to_bytes().to_vec()), + Value::LegacySm2Configuration(c) => { + (AdminValueKind::LegacySm2Configuration, c.raw().to_vec()) + } + Value::PinStatus(v) => ( + AdminValueKind::PinStatus, + vec![ + u8::from(v.verified), + v.retries_remaining.unwrap_or(0xff), + u8::from(v.blocked), + ], + ), + Value::KeyboardLayout(v) => (AdminValueKind::KeyboardLayout, vec![v]), + Value::KeyboardKeymap(v) => (AdminValueKind::KeyboardKeymap, v.as_bytes().to_vec()), + Value::PassSlots(v) => (AdminValueKind::PassSlots, pass_slots_data(v)), + }; + AdminResult { + kind, + data, + progress, + } +} + +/// PASS slot states are self-delimiting, but each state is additionally +/// length-prefixed so Dart never re-implements the type dispatch: +/// `short_len | short | long_len | long`. One state is its type byte plus +/// payload: 0x00 off; 0x02 enter; 0x03 (HMAC-SHA1); 0x01 len name enter; +/// any other byte is an unknown type carried verbatim. Firmware never +/// returns secret slot material, so no secret bytes cross the bridge here. +fn pass_slots_data(slots: admin::PassSlots) -> Vec { + fn state(state: admin::PassSlotState) -> Vec { + use admin::PassSlotState; + match state { + PassSlotState::Off => vec![0x00], + PassSlotState::Static { append_enter } => vec![0x02, u8::from(append_enter)], + PassSlotState::HmacSha1 => vec![0x03], + PassSlotState::Oath { name, append_enter } => { + let mut data = Vec::with_capacity(name.len() + 3); + data.push(0x01); + data.push(name.len() as u8); + data.extend_from_slice(&name); + data.push(u8::from(append_enter)); + data + } + PassSlotState::Unknown(kind) => vec![kind], + } + } + let short = state(slots.short); + let long = state(slots.long); + let mut data = Vec::with_capacity(short.len() + long.len() + 2); + data.push(short.len() as u8); + data.extend(short); + data.push(long.len() as u8); + data.extend(long); + data } /// Structured, payload-free protocol failure. Transport errors remain in Dart. @@ -38,6 +216,8 @@ pub struct ProtocolStep { pub command: Option>, pub data: Option>, pub error: Option, + pub profile: Option, + pub admin: Option, } impl ProtocolStep { @@ -46,179 +226,3576 @@ impl ProtocolStep { command: None, data: None, error: Some(error.into()), + profile: None, + admin: None, } } } enum Inner { Bytes(Operation), + PinStatus(Operation), Configuration(Operation), + Certificate(Operation), + Probe(Operation), + Metadata(Operation), + Directory(Operation), + PublicKey(Operation), + Signature(Operation), + Sm2Agreement(Operation), + Oath(Operation), + OpenPgp(Operation), + Credential(Operation), + Management(Operation<()>), + Admin(Operation), + BootstrapSelect(Operation), + BootstrapSerial(Operation), + NdefCapability(Operation), + NdefMessage(Operation), + Ctap(Operation), } -/// A single caller-owned operation, retained across Dart await points. -/// Close is idempotent and releases Rust buffers without sending any APDU. +/// Immutable observations of one device. Dart binds this owner to a card lease. +/// It is not an authentication token; constructors copy their required evidence. #[flutter_rust_bridge::frb(opaque)] -pub struct ProtocolOperation { - // FRB opaque handles require Sync, while core operations are only Send. - // All entry points take &mut self; get_mut never locks or spans Dart I/O. - inner: Mutex>, - construction_error: Option, +pub struct ProtocolProfile { + inner: Option, } -impl ProtocolOperation { - /// Construct without I/O. Only Select changes applet selection; reads never - /// probe, SELECT, authenticate, or retain a device profile implicitly. +impl ProtocolProfile { + /// Bootstrap observations, not an authenticated session or cached credential. #[flutter_rust_bridge::frb(sync)] - pub fn piv_read(kind: PivReadOperation) -> Self { - let options = OperationOptions::default(); - let operation = match kind { - PivReadOperation::Select => piv::select_application(options).map(Inner::Bytes), - PivReadOperation::Version => piv::read_version_selected(options).map(Inner::Bytes), - PivReadOperation::AlgorithmConfiguration => { - piv::read_configuration_selected(options).map(Inner::Configuration) - } + pub fn firmware(&self) -> Option> { + self.inner + .as_ref() + .map(|p| p.info().firmware_text().to_vec()) + } + #[flutter_rust_bridge::frb(sync)] + pub fn model(&self) -> Option { + self.inner.as_ref()?.info().model().map(str::to_owned) + } + /// `existing` reuses the caller's selected, already authorized Admin + /// transaction and forbids a PIN; otherwise the operation SELECTs and + /// verifies only an explicitly supplied PIN. + #[flutter_rust_bridge::frb(sync)] + pub fn admin_read( + &self, + kind: AdminReadOperation, + pin: Option>, + existing: bool, + ) -> ProtocolOperation { + use admin::Request; + let request = match kind { + AdminReadOperation::Firmware => Request::Firmware, + AdminReadOperation::Model => Request::Model, + AdminReadOperation::Serial => Request::Serial, + AdminReadOperation::ChipId => Request::ChipId, + AdminReadOperation::CoreCommit => Request::CoreCommit, + AdminReadOperation::Configuration => Request::Configuration, + AdminReadOperation::FlashUsage => Request::FlashUsage, + AdminReadOperation::AppletUsage => Request::AppletUsage, + AdminReadOperation::KeyboardLayout => Request::KeyboardLayout, + AdminReadOperation::KeyboardKeymap => Request::KeyboardKeymap, + AdminReadOperation::NfcStatus => Request::NfcStatus, + AdminReadOperation::Sm2Configuration => Request::Sm2Configuration, }; - match operation { - Ok(inner) => Self { - inner: Mutex::new(Some(inner)), - construction_error: None, - }, - Err(error) => Self { - inner: Mutex::new(None), - construction_error: Some(error), - }, - } + self.admin_operation(Ok(request), pin, existing) + } + /// `existing` reuses the caller's authorized Admin transaction and forbids + /// a PIN; otherwise the operation SELECTs and verifies the supplied PIN. + #[flutter_rust_bridge::frb(sync)] + pub fn admin_configure( + &self, + patch: AdminConfigurationPatch, + pin: Option>, + existing: bool, + ) -> ProtocolOperation { + self.admin_operation( + Ok(admin::Request::Configure(admin::ConfigurationPatch { + led_on: patch.led_on, + ndef_read_only: patch.ndef_read_only, + ndef_enabled: patch.ndef_enabled, + webusb_landing: patch.webusb_landing, + feature_mask: patch.feature_mask, + feature_values: patch.feature_values, + })), + pin, + existing, + ) + } + #[flutter_rust_bridge::frb(sync)] + pub fn admin_action( + &self, + kind: AdminAction, + pin: Option>, + data: Vec, + index: u8, + value: u8, + existing: bool, + ) -> ProtocolOperation { + let data = SecretBytes::new(data); + let request = (|| { + use admin::Request; + let boolean = || match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::new(ErrorKind::InvalidArgument)), + }; + Ok(match kind { + AdminAction::VerifyPin => Request::VerifyPin, + AdminAction::ChangePin => { + Request::ChangePin(admin::Pin::from_bytes(data.as_bytes())?) + } + AdminAction::KeyboardInterface => Request::SetKeyboardInterface(boolean()?), + AdminAction::KeyboardReturn => Request::SetKeyboardReturn(boolean()?), + AdminAction::KeyboardKeymap => { + let keymap: [u8; 256] = data + .as_bytes() + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + Request::SetKeyboardKeymap { + layout_id: index, + keymap: admin::KeyboardKeymap::from_bytes(&keymap)?, + } + } + AdminAction::ClearKeyboardKeymap => Request::ClearKeyboardKeymap, + AdminAction::LegacyPivExtensions => Request::SetLegacyPivExtensions(boolean()?), + AdminAction::LegacyTouch => Request::SetLegacyOpenPgpTouch(match index { + 0 => admin::LegacyOpenPgpTouch::Signature(boolean()?), + 1 => admin::LegacyOpenPgpTouch::Decryption(boolean()?), + 2 => admin::LegacyOpenPgpTouch::Authentication(boolean()?), + 3 => admin::LegacyOpenPgpTouch::CacheSeconds(value), + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }), + AdminAction::WriteSm2 => { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + if profile + .capability(canokey::compatibility::Capability::AdminLegacySm2) + .support + == canokey::compatibility::Support::Supported + { + Request::WriteLegacySm2(admin::LegacySm2Configuration::from_bytes( + data.as_bytes(), + )?) + } else { + let c = admin::Sm2Configuration::parse(data.as_bytes())?; + Request::ConfigureSm2(admin::Sm2Patch { + curve_id: Some(c.curve_id), + algorithm_id: Some(c.algorithm_id), + }) + } + } + AdminAction::Nfc => Request::SetNfc(boolean()?), + AdminAction::ResetOpenPgp => Request::ResetApplet(admin::Applet::OpenPgp), + AdminAction::ResetPiv => Request::ResetApplet(admin::Applet::Piv), + AdminAction::ResetOath => Request::ResetApplet(admin::Applet::Oath), + AdminAction::ResetNdef => Request::ResetApplet(admin::Applet::Ndef), + AdminAction::ResetCtap => Request::ResetApplet(admin::Applet::Ctap), + AdminAction::ResetPass => Request::ResetApplet(admin::Applet::Pass), + AdminAction::FactoryReset => Request::FactoryReset, + }) + })(); + self.admin_operation(request, pin, existing) } - /// Start exactly once; subsequent calls report an operation-state error. + /// Read both typed PASS slot configurations (INS 43). `existing` reuses the + /// caller's selected, already authorized Admin transaction and forbids a + /// PIN; otherwise the operation SELECTs and verifies the supplied PIN. + /// Secret slot material is never returned by firmware. #[flutter_rust_bridge::frb(sync)] - pub fn start(&mut self) -> ProtocolStep { - self.drive(None) + pub fn admin_pass_slots(&self, pin: Option>, existing: bool) -> ProtocolOperation { + self.admin_operation(Ok(admin::Request::PassSlots), pin, existing) } - /// Consume one complete, unprocessed response including SW1/SW2. - /// The input is copied into zeroizing storage and released before returning. + /// Replace one PASS slot (INS 44): slot 0 = short touch, 1 = long touch; + /// kind 0 = off, 1 = static password (data, append_enter), 2 = HMAC-SHA1 + /// (data = 20-byte key). OATH slots are configured through the OATH + /// applet, never here. The write is validated before any I/O. #[flutter_rust_bridge::frb(sync)] - pub fn advance(&mut self, response: Vec) -> ProtocolStep { - let response = SecretBytes::new(response); - self.drive(Some(response.as_bytes())) + pub fn admin_set_pass_slot( + &self, + slot: u8, + kind: u8, + data: Vec, + append_enter: bool, + pin: Option>, + existing: bool, + ) -> ProtocolOperation { + let data = SecretBytes::new(data); + let request = (move || { + let slot = match slot { + 0 => admin::PassSlotId::Short, + 1 => admin::PassSlotId::Long, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let config = match kind { + 0 => admin::PassSlotConfig::Off, + 1 => admin::PassSlotConfig::Static { + password: data, + append_enter, + }, + 2 => admin::PassSlotConfig::HmacSha1 { key: data }, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + Ok(admin::Request::SetPassSlot { slot, config }) + })(); + self.admin_operation(request, pin, existing) + } + fn admin_operation( + &self, + request: Result, + pin: Option>, + existing: bool, + ) -> ProtocolOperation { + let pin = pin.map(SecretBytes::new); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let pin = pin + .as_ref() + .map(|p| admin::Pin::from_bytes(p.as_bytes())) + .transpose()?; + // Existing reuses the caller's selected, already authorized Admin + // transaction; carrying a PIN under it would be meaningless. + let access = match (existing, pin) { + (true, None) => admin::Access::Existing, + (true, Some(_)) => return Err(Error::new(ErrorKind::InvalidArgument)), + (false, None) => admin::Access::None, + (false, Some(pin)) => admin::Access::Pin(pin), + }; + admin::operation_with_access(profile, request?, access, OperationOptions::default()) + .map(Inner::Admin) + })()) } - /// Release locally, including on a transport exception. Never retry/rollback. + /// The serial observed during explicit discovery. Missing observations stay absent. #[flutter_rust_bridge::frb(sync)] - pub fn close(&mut self) { - self.inner - .get_mut() - .unwrap_or_else(|e| e.into_inner()) - .take(); - self.construction_error.take(); + pub fn serial(&self) -> Option> { + self.inner.as_ref()?.info().serial().map(<[u8]>::to_vec) } -} -impl ProtocolOperation { - fn drive(&mut self, response: Option<&[u8]>) -> ProtocolStep { - if let Some(error) = self.construction_error.take() { - return ProtocolStep::failure(error); - } - let result = match self - .inner - .get_mut() - .unwrap_or_else(|e| e.into_inner()) - .as_mut() - { - Some(Inner::Bytes(op)) => drive(op, response, |data| data.as_bytes().to_vec()), - Some(Inner::Configuration(op)) => drive(op, response, |data| data.raw().to_vec()), - None => Err(Error::new(ErrorKind::OperationStateError)), - }; - result.unwrap_or_else(ProtocolStep::failure) + /// Credentials are copied into upstream zeroizing owners before returning. + /// Success does not turn this immutable profile into an authorization token. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_credential( + &self, + kind: PivCredentialOperation, + current: Vec, + replacement: Vec, + ) -> ProtocolOperation { + let current = SecretBytes::new(current); + let replacement = SecretBytes::new(replacement); + let operation = (|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + use piv::CredentialAction; + let action = match kind { + PivCredentialOperation::VerifyPin => { + CredentialAction::VerifyPin(piv::Pin::from_bytes(current.as_bytes())?) + } + PivCredentialOperation::ChangePin => CredentialAction::ChangePin { + old: piv::Pin::from_bytes(current.as_bytes())?, + new: piv::Pin::from_bytes(replacement.as_bytes())?, + }, + PivCredentialOperation::ChangePuk => CredentialAction::ChangePuk { + old: piv::Puk::from_bytes(current.as_bytes())?, + new: piv::Puk::from_bytes(replacement.as_bytes())?, + }, + PivCredentialOperation::UnblockPin => CredentialAction::UnblockPin { + puk: piv::Puk::from_bytes(current.as_bytes())?, + new_pin: piv::Pin::from_bytes(replacement.as_bytes())?, + }, + PivCredentialOperation::Logout => CredentialAction::Logout, + }; + piv::credential(profile, action, false, OperationOptions::default()) + .map(Inner::Credential) + })(); + ProtocolOperation::from_operation(operation) } -} -fn drive( - op: &mut Operation, - response: Option<&[u8]>, - encode: impl FnOnce(T) -> Vec, -) -> Result { - let step = match response { - Some(response) => op.advance(response)?, - None => op.start()?, - }; - Ok(match step { - Step::Exchange => ProtocolStep { - command: Some(op.command()?.as_bytes().to_vec()), - data: None, - error: None, - }, - Step::Done => ProtocolStep { - command: None, - data: Some(encode(op.take_result()?)), - error: None, - }, - }) -} + /// Preserve Console's explicit external authentication mode. libcanokey owns + /// the full challenge exchange and cryptography; no host login is inferred. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_authenticate_management(&self, algorithm: u8, key: Vec) -> ProtocolOperation { + let key = SecretBytes::new(key); + let operation = (|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = match algorithm { + 0x03 => piv::ManagementKeyAlgorithm::Tdes, + 0x0a => piv::ManagementKeyAlgorithm::Aes192, + _ => return Err(Error::new(ErrorKind::UnsupportedAlgorithm)), + }; + let key = piv::ManagementKey::from_bytes(algorithm, key.as_bytes())?; + piv::authenticate_management_key( + profile, + piv::ManagementAuthentication::external(key), + false, + OperationOptions::default(), + ) + .map(Inner::Management) + })(); + ProtocolOperation::from_operation(operation) + } + /// Read metadata in the caller's already selected PIV session. No probing, + /// SELECT or authentication is inserted, including after VERIFY. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_metadata(&self, reference: u8) -> ProtocolOperation { + let operation = (|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reference = match reference { + 0x80 => piv::MetadataReference::Pin, + 0x81 => piv::MetadataReference::Puk, + 0x9b => piv::MetadataReference::Management, + value => piv::MetadataReference::Key(piv_slot(value)?), + }; + piv::get_metadata( + profile, + reference, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Metadata) + })(); + ProtocolOperation::from_operation(operation) + } -#[cfg(test)] -mod tests { - use super::*; + /// Read the PIV metadata directory in the caller's selected transaction. + /// The upstream parser validates framing and preserves the raw directory. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_metadata_directory(&self) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::read_metadata_directory( + profile, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Directory) + })()) + } - #[test] - fn selected_version_continuation_and_le_correction() { - let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); - assert_eq!(op.start().command.unwrap(), [0, 0xfd, 0, 0, 0]); - assert_eq!( - op.advance(vec![0x6c, 3]).command.unwrap(), - [0, 0xfd, 0, 0, 3] - ); - assert_eq!( - op.advance(vec![6, 0x61, 2]).command.unwrap(), - [0, 0xc0, 0, 0, 2] - ); - assert_eq!(op.advance(vec![0, 0, 0x90, 0]).data.unwrap(), [6, 0, 0]); - assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); - op.close(); - op.close(); - assert_eq!( - op.advance(vec![0x90, 0]).error.unwrap().kind, - "OperationStateError" - ); + #[flutter_rust_bridge::frb(sync)] + pub fn piv_generate_key( + &self, + slot: u8, + algorithm: u8, + pin_policy: u8, + touch_policy: u8, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let parameters = piv::KeyParameters { + slot: piv_slot(slot)?, + algorithm: profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?, + pin_policy: match pin_policy { + 0 => piv::PinPolicy::Default, + 1 => piv::PinPolicy::Never, + 2 => piv::PinPolicy::Once, + 3 => piv::PinPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + touch_policy: match touch_policy { + 0 => piv::TouchPolicy::Default, + 1 => piv::TouchPolicy::Never, + 2 => piv::TouchPolicy::Always, + 3 => piv::TouchPolicy::Cached, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + }; + piv::generate_key( + profile, + parameters, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::PublicKey) + })()) } - #[test] - fn select_and_structured_failure() { - let mut op = ProtocolOperation::piv_read(PivReadOperation::Select); - assert_eq!( - op.start().command.unwrap(), - [0, 0xa4, 4, 0, 5, 0xa0, 0, 0, 3, 8, 0] - ); - let error = op.advance(vec![0x6a, 0x82]).error.unwrap(); - assert_eq!(error.kind, "UnsupportedDevice"); - assert_eq!(error.phase, "Select"); - assert_eq!(error.status_word, Some(0x6a82)); + /// Reuse the caller's selected transaction; the profile is evidence only. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_certificate(&self, object_id: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = match object_id { + 0x05 => piv::Slot::Authentication, + 0x0a => piv::Slot::Signature, + 0x0b => piv::Slot::KeyManagement, + 0x01 => piv::Slot::CardAuthentication, + 0x0d..=0x20 => piv::Slot::Retired(piv::RetiredSlot::new(object_id - 0x0c)?), + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + piv::read_certificate( + profile, + slot, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Certificate) + })()) } - #[test] - fn malformed_and_oversized_responses_are_terminal() { - for response in [vec![0x90], vec![0, 0x90, 0], vec![0; 259]] { - let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); - op.start(); - assert!(op.advance(response).error.is_some()); - assert_eq!( - op.advance(vec![6, 0, 0, 0x90, 0]).error.unwrap().kind, - "OperationStateError" - ); - } + #[flutter_rust_bridge::frb(sync)] + pub fn piv_delete_certificate(&self, object_id: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::delete_certificate( + profile, + piv_slot(object_id)?, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) } - #[test] - fn configuration_uses_upstream_validation() { - let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); - assert_eq!(op.start().command.unwrap(), [0, 0xee, 1, 0, 0]); - assert!(op.advance(vec![1, 0x90, 0]).error.is_some()); - let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); - op.start(); - let bytes = vec![1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3]; - let mut response = bytes.clone(); - response.extend([0x90, 0]); - assert_eq!(op.advance(response).data.unwrap(), bytes); + #[flutter_rust_bridge::frb(sync)] + pub fn piv_delete_key(&self, slot: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::delete_key( + profile, + piv_slot(slot)?, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_move_key(&self, source: u8, target: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::move_key( + profile, + piv_slot(source)?, + piv_slot(target)?, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_set_management_key( + &self, + algorithm: u8, + key: Vec, + touch: u8, + update_protected: bool, + ) -> ProtocolOperation { + let key = SecretBytes::new(key); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let alg = match algorithm { + 0x03 => piv::ManagementKeyAlgorithm::Tdes, + 0x0a => piv::ManagementKeyAlgorithm::Aes192, + _ => return Err(Error::new(ErrorKind::UnsupportedAlgorithm)), + }; + let key = piv::ManagementKey::from_bytes(alg, key.as_bytes())?; + let touch = match touch { + 0 => piv::ManagementTouchPolicy::Never, + 1 => piv::ManagementTouchPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + piv::set_management_key( + profile, + key, + touch, + update_protected, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_set_algorithm_config(&self, raw: Vec) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let config = canokey::compatibility::AlgorithmConfig::parse(&raw)?; + piv::set_algorithm_config( + profile, + config, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_set_container_name(&self, slot: u8, name: String) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let name = piv::ContainerName::from_text(&name)?; + piv::set_container_name( + profile, + piv_slot(slot)?, + name, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_reset_pin_puk_retries(&self, pin_retries: u8, puk_retries: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::reset_pin_puk_retries( + profile, + pin_retries, + puk_retries, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_sign( + &self, + slot: u8, + algorithm: u8, + input: Vec, + input_kind: u8, + ) -> ProtocolOperation { + let input = SecretBytes::new(input); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?; + let input = match input_kind { + 0 => piv::SignInput::RsaEncodedBlock(input), + 1 => piv::SignInput::Digest(input), + 2 => piv::SignInput::Message(input), + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + piv::sign( + profile, + piv_slot(slot)?, + algorithm, + input, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Signature) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_decrypt(&self, slot: u8, algorithm: u8, ciphertext: Vec) -> ProtocolOperation { + let ciphertext = SecretBytes::new(ciphertext); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?; + piv::decrypt( + profile, + piv_slot(slot)?, + algorithm, + ciphertext, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Bytes) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_derive(&self, slot: u8, algorithm: u8, peer: Vec) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?; + piv::derive( + profile, + piv_slot(slot)?, + algorithm, + peer, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Bytes) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_decapsulate(&self, slot: u8, ciphertext: Vec) -> ProtocolOperation { + let ciphertext = SecretBytes::new(ciphertext); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::decapsulate( + profile, + piv_slot(slot)?, + ciphertext, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Bytes) + })()) + } + + #[allow(clippy::too_many_arguments)] + #[flutter_rust_bridge::frb(sync)] + pub fn piv_sm2_agreement( + &self, + slot: u8, + role: u8, + peer_static: Vec, + peer_ephemeral: Vec, + user_id: Option>, + peer_id: Option>, + key_len: u16, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let role = match role { + 0 => piv::Sm2Role::Initiator, + 1 => piv::Sm2Role::Responder, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let input = piv::Sm2AgreementInput { + role, + peer_static, + peer_ephemeral, + user_id, + peer_id, + key_len, + }; + piv::agree_sm2( + profile, + piv_slot(slot)?, + input, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Sm2Agreement) + })()) + } + + /// INS F9 attestation certificate for a generated slot. The selected-context + /// factory asserts no credential and consumes no authentication state. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_attest(&self, slot: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::attest(profile, piv_slot(slot)?, false, OperationOptions::default()) + .map(Inner::Bytes) + })()) + } + + /// Firmware streaming signature modes: 0 = ML-DSA-65 with the empty context, + /// 1 = randomized Ed25519 (wire mode FF), 2 = SM2 full message with an + /// optional user ID. Classic digest/padded signing stays with `piv_sign`. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_sign_streaming( + &self, + slot: u8, + mode: u8, + message: Vec, + user_id: Option>, + ) -> ProtocolOperation { + let message = SecretBytes::new(message); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let input = match mode { + 0 => piv::StreamingSignInput::MlDsa65(message), + 1 => piv::StreamingSignInput::Ed25519Randomized(message), + 2 => piv::StreamingSignInput::Sm2 { message, user_id }, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + piv::sign_streaming( + profile, + piv_slot(slot)?, + input, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Signature) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_import_ec_key( + &self, + slot: u8, + algorithm: u8, + scalar: Vec, + pin_policy: u8, + touch_policy: u8, + ) -> ProtocolOperation { + let scalar = SecretBytes::new(scalar); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?; + let parameters = piv::KeyParameters { + slot: piv_slot(slot)?, + algorithm, + pin_policy: match pin_policy { + 0 => piv::PinPolicy::Default, + 1 => piv::PinPolicy::Never, + 2 => piv::PinPolicy::Once, + 3 => piv::PinPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + touch_policy: match touch_policy { + 0 => piv::TouchPolicy::Default, + 1 => piv::TouchPolicy::Never, + 2 => piv::TouchPolicy::Always, + 3 => piv::TouchPolicy::Cached, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + }; + let material = piv::PrivateKeyMaterial::ec_scalar(algorithm, scalar.as_bytes())?; + piv::import_key( + profile, + parameters, + material, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[allow(clippy::too_many_arguments)] + #[flutter_rust_bridge::frb(sync)] + pub fn piv_import_rsa_key( + &self, + slot: u8, + algorithm: u8, + p: Vec, + q: Vec, + dp: Vec, + dq: Vec, + qinv: Vec, + pin_policy: u8, + touch_policy: u8, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = profile + .algorithm_from_wire_id(algorithm) + .ok_or_else(|| Error::new(ErrorKind::UnsupportedAlgorithm))?; + let parameters = piv::KeyParameters { + slot: piv_slot(slot)?, + algorithm, + pin_policy: match pin_policy { + 0 => piv::PinPolicy::Default, + 1 => piv::PinPolicy::Never, + 2 => piv::PinPolicy::Once, + 3 => piv::PinPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + touch_policy: match touch_policy { + 0 => piv::TouchPolicy::Default, + 1 => piv::TouchPolicy::Never, + 2 => piv::TouchPolicy::Always, + 3 => piv::TouchPolicy::Cached, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + }; + let material = piv::PrivateKeyMaterial::rsa_crt(algorithm, [&p, &q, &dp, &dq, &qinv])?; + piv::import_key( + profile, + parameters, + material, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn piv_import_ed25519_key( + &self, + slot: u8, + seed: Vec, + pin_policy: u8, + touch_policy: u8, + ) -> ProtocolOperation { + let seed = SecretBytes::new(seed); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let algorithm = piv::Algorithm::Ed25519; + let parameters = piv::KeyParameters { + slot: piv_slot(slot)?, + algorithm, + pin_policy: match pin_policy { + 0 => piv::PinPolicy::Default, + 1 => piv::PinPolicy::Never, + 2 => piv::PinPolicy::Once, + 3 => piv::PinPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + touch_policy: match touch_policy { + 0 => piv::TouchPolicy::Default, + 1 => piv::TouchPolicy::Never, + 2 => piv::TouchPolicy::Always, + 3 => piv::TouchPolicy::Cached, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + }; + let material = piv::PrivateKeyMaterial::ed25519_seed(seed.as_bytes())?; + piv::import_key( + profile, + parameters, + material, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + /// Import a post-quantum seed (INS FE): kind 0 = ML-DSA-65 (32-byte seed), + /// 1 = ML-KEM-768 (64-byte d||z seed). Semantics match the EC/RSA/Ed25519 + /// imports: Access::Existing inside the caller's reserved transaction. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_import_pq_seed( + &self, + slot: u8, + kind: u8, + seed: Vec, + pin_policy: u8, + touch_policy: u8, + ) -> ProtocolOperation { + let seed = SecretBytes::new(seed); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let material = match kind { + 0 => piv::PrivateKeyMaterial::mldsa65_seed(seed.as_bytes())?, + 1 => piv::PrivateKeyMaterial::mlkem768_seed(seed.as_bytes())?, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let parameters = piv::KeyParameters { + slot: piv_slot(slot)?, + algorithm: material.algorithm(), + pin_policy: match pin_policy { + 0 => piv::PinPolicy::Default, + 1 => piv::PinPolicy::Never, + 2 => piv::PinPolicy::Once, + 3 => piv::PinPolicy::Always, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + touch_policy: match touch_policy { + 0 => piv::TouchPolicy::Default, + 1 => piv::TouchPolicy::Never, + 2 => piv::TouchPolicy::Always, + 3 => piv::TouchPolicy::Cached, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }, + }; + piv::import_key( + profile, + parameters, + material, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + /// Mark an existing HOTP credential as the touch keyboard-emulation + /// default: slot 0 = short, 1 = long. The legacy (pre-3.0) single-slot + /// dialect is chosen from the probed profile; a long slot or append-enter + /// there fails construction with InvalidArgument before any I/O. + #[flutter_rust_bridge::frb(sync)] + pub fn oath_set_default( + &self, + slot: u8, + append_enter: bool, + name: String, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = match slot { + 0 => oath::DefaultSlot::Short, + 1 => oath::DefaultSlot::Long, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let request = oath::Request::SetDefault { + slot, + append_enter, + name: oath::Name::from_bytes(name.as_bytes())?, + }; + oath::operation( + profile, + request, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_delete( + &self, + name: Vec, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let request = oath::Request::Delete(oath::Name::from_bytes(&name)?); + oath::operation( + profile, + request, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_rename( + &self, + old: Vec, + new: Vec, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let request = oath::Request::Rename { + old: oath::Name::from_bytes(&old)?, + new: oath::Name::from_bytes(&new)?, + }; + oath::operation( + profile, + request, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_list( + &self, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + oath::operation( + profile, + oath::Request::List, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_select(&self) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + oath::operation( + profile, + oath::Request::Select, + None, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_validate(&self, key: Vec, challenge: Vec) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let key = oath::AccessKey::from_bytes(&key)?; + let challenge: [u8; 8] = challenge + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + oath::operation( + profile, + oath::Request::Validate, + Some(oath::Access { key, challenge }), + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_logout(&self, reference: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reference = match reference { + 0 => openpgp::PasswordReference::Pw1Sign, + 1 => openpgp::PasswordReference::Pw1Other, + 2 => openpgp::PasswordReference::Pw3, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + openpgp::operation( + profile, + openpgp::Request::Logout(reference), + None, + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_read_data(&self, tag: u16) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + openpgp::operation( + profile, + openpgp::Request::ReadData(tag), + None, + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_verify(&self, reference: u8, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reference = match reference { + 0 => openpgp::PasswordReference::Pw1Sign, + 1 => openpgp::PasswordReference::Pw1Other, + 2 => openpgp::PasswordReference::Pw3, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::Verify, + Some(openpgp::Access { + reference, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_read_certificate(&self, slot: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = match slot { + 0 => openpgp::Slot::Signature, + 1 => openpgp::Slot::Decryption, + 2 => openpgp::Slot::Authentication, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + openpgp::operation( + profile, + openpgp::Request::ReadCertificate(slot), + None, + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_certificate( + &self, + slot: u8, + certificate: Vec, + password: Vec, + ) -> ProtocolOperation { + let certificate = SecretBytes::new(certificate); + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = match slot { + 0 => openpgp::Slot::Signature, + 1 => openpgp::Slot::Decryption, + 2 => openpgp::Slot::Authentication, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteCertificate(slot, certificate), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_change_password( + &self, + reference: u8, + old: Vec, + new: Vec, + ) -> ProtocolOperation { + let old = SecretBytes::new(old); + let new = SecretBytes::new(new); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reference = match reference { + 0 => openpgp::PasswordReference::Pw1Sign, + 2 => openpgp::PasswordReference::Pw3, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let request = openpgp::Request::ChangePassword { + reference, + old: openpgp::Password::from_bytes(old.as_bytes())?, + new: openpgp::Password::from_bytes(new.as_bytes())?, + }; + openpgp::operation(profile, request, None, OperationOptions::default()) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_pin_status(&self, reference: u8) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reference = match reference { + 0 => openpgp::PasswordReference::Pw1Sign, + 1 => openpgp::PasswordReference::Pw1Other, + 2 => openpgp::PasswordReference::Pw3, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + openpgp::operation( + profile, + openpgp::Request::PinStatus(reference), + None, + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_name(&self, name: Vec, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::Name(name)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_login(&self, login: Vec, password: Vec) -> ProtocolOperation { + let login = SecretBytes::new(login); + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::Login(login)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_language( + &self, + language: Vec, + password: Vec, + ) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::Language(language)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_sex(&self, sex: u8, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::Sex(sex)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_url(&self, url: Vec, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::Url(url)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_generate_key(&self, slot: u8, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = match slot { + 0 => openpgp::Slot::Signature, + 1 => openpgp::Slot::Decryption, + 2 => openpgp::Slot::Authentication, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::GenerateKey(slot), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_terminate(&self, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::Terminate, + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_activate(&self, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::Activate, + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Set (Some) or clear (None) the reset code after explicit PW3 verification. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_reset_code( + &self, + reset_code: Option>, + password: Vec, + ) -> ProtocolOperation { + let reset_code = reset_code.map(SecretBytes::new); + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let reset_code = reset_code + .map(|code| openpgp::Password::from_bytes(code.as_bytes())) + .transpose()?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::ResetCode(reset_code)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Set PW1/reset-code/PW3 retry limits after explicit PW3 verification. + /// The card resets PW1/PW3 to firmware defaults and clears authorization; + /// the retry-reset capability gates construction before any I/O. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_reset_retries(&self, retries: Vec, password: Vec) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let retries: [u8; 3] = retries + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::ResetRetries(retries), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Whether one explicit PW1-sign verification may authorize multiple + /// signatures (PW status bytes policy). Written after PW3 verification. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_signature_pin_policy( + &self, + reuse: bool, + password: Vec, + ) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::ReuseSignaturePin(reuse)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Reset PW1 after explicit PW3 verification; does not change PW3. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_unblock_with_admin( + &self, + new_pin: Vec, + password: Vec, + ) -> ProtocolOperation { + let new_pin = SecretBytes::new(new_pin); + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let new_pin = openpgp::Password::from_bytes(new_pin.as_bytes())?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::UnblockWithAdmin(new_pin), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Reset PW1 using reset-code||new-PW1; no password verification is inserted. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_unblock_with_code(&self, code: Vec, new_pin: Vec) -> ProtocolOperation { + let code = SecretBytes::new(code); + let new_pin = SecretBytes::new(new_pin); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let code = openpgp::Password::from_bytes(code.as_bytes())?; + let new_pin = openpgp::Password::from_bytes(new_pin.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::UnblockWithCode { code, new: new_pin }, + None, + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Slot touch policy (0 off, 1 on, 2 permanent) after PW3 verification. + /// Emits the standard two-byte UIF field; the UIF capability gates it. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_touch_policy( + &self, + slot: u8, + policy: u8, + password: Vec, + ) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let slot = openpgp_slot(slot)?; + let policy = match policy { + 0 => openpgp::TouchPolicy::Off, + 1 => openpgp::TouchPolicy::On, + 2 => openpgp::TouchPolicy::Permanent, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::TouchPolicy(slot, policy)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + /// Touch cache duration in seconds after PW3 verification; UIF-gated. + #[flutter_rust_bridge::frb(sync)] + pub fn openpgp_write_touch_cache_time( + &self, + seconds: u8, + password: Vec, + ) -> ProtocolOperation { + let password = SecretBytes::new(password); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let password = openpgp::Password::from_bytes(password.as_bytes())?; + openpgp::operation( + profile, + openpgp::Request::WriteData(openpgp::DataWrite::TouchCacheTime(seconds)), + Some(openpgp::Access { + reference: openpgp::PasswordReference::Pw3, + password, + }), + OperationOptions::default(), + ) + .map(Inner::OpenPgp) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_clear_code( + &self, + key: Option>, + challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + oath::operation( + profile, + oath::Request::ClearCode, + oath_access(key, challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + /// The current key validates in the same operation only when supplied; + /// setting the first code on an unprotected applet passes no old key. + #[flutter_rust_bridge::frb(sync)] + pub fn oath_set_code( + &self, + old_key: Option>, + new_key: Vec, + challenge: Vec, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let new_key = oath::AccessKey::from_bytes(&new_key)?; + let challenge: [u8; 8] = challenge + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + let access = old_key + .map(|old_key| { + let key = oath::AccessKey::from_bytes(&old_key)?; + Ok::<_, Error>(oath::Access { key, challenge }) + }) + .transpose()?; + oath::operation( + profile, + oath::Request::SetCode { + key: new_key, + challenge, + }, + access, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[allow(clippy::too_many_arguments)] + #[flutter_rust_bridge::frb(sync)] + pub fn oath_put( + &self, + name: Vec, + kind: u8, + algorithm: u8, + digits: u8, + secret: Vec, + require_touch: bool, + increasing: bool, + initial_counter: u32, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let kind = match kind { + 1 => oath::Kind::Hotp, + 2 => oath::Kind::Totp, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let algorithm = match algorithm { + 1 => oath::Algorithm::Sha1, + 2 => oath::Algorithm::Sha256, + 3 => oath::Algorithm::Sha512, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let request = oath::Request::Put(oath::Credential { + name: oath::Name::from_bytes(&name)?, + kind, + algorithm, + digits, + secret: SecretBytes::new(secret), + require_touch, + increasing, + initial_counter, + }); + oath::operation( + profile, + request, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[allow(clippy::too_many_arguments)] + #[flutter_rust_bridge::frb(sync)] + pub fn oath_calculate( + &self, + name: Vec, + kind: u8, + algorithm: u8, + challenge: Option>, + format: u8, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let kind = match kind { + 1 => oath::Kind::Hotp, + 2 => oath::Kind::Totp, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let algorithm = match algorithm { + 1 => oath::Algorithm::Sha1, + 2 => oath::Algorithm::Sha256, + 3 => oath::Algorithm::Sha512, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let challenge = challenge + .map(|v| { + v.try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument)) + }) + .transpose()?; + let format = match format { + 0 => oath::Format::Truncated, + 1 => oath::Format::Full, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + let request = oath::Request::Calculate { + name: oath::Name::from_bytes(&name)?, + kind, + algorithm, + challenge, + format, + }; + oath::operation( + profile, + request, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn oath_calculate_all( + &self, + challenge: Vec, + format: u8, + access_key: Option>, + access_challenge: Option>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let challenge: [u8; 8] = challenge + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + let format = match format { + 0 => oath::Format::Truncated, + 1 => oath::Format::Full, + _ => return Err(Error::new(ErrorKind::InvalidArgument)), + }; + oath::operation( + profile, + oath::Request::CalculateAll { challenge, format }, + oath_access(access_key, access_challenge)?, + OperationOptions::default(), + ) + .map(Inner::Oath) + })()) + } + + /// Return the normalized object value. libcanokey owns framing and parsing. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_read_object(&self, object_id: Vec) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::read_object( + profile, + piv::ObjectId::from_bytes(&object_id)?, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Bytes) + })()) + } + + /// Writes may be partially applied before failure; never retry or roll back. + /// Existing delegates authorization to the card in the caller-owned lease. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_write_object(&self, object_id: Vec, data: Vec) -> ProtocolOperation { + let data = SecretBytes::new(data); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + piv::write_object( + profile, + piv::ObjectId::from_bytes(&object_id)?, + data, + piv::Access::Existing, + OperationOptions::default(), + ) + .map(Inner::Credential) + })()) + } + + /// Resolve an observed key algorithm to Console's fixed display enum IDs. + /// These are NOT device wire IDs and never authorize a key operation. + /// Unknown algorithms and closed profiles return None. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_algorithm_display_id(&self, wire_id: u8) -> Option { + use piv::Algorithm; + Some( + match self.inner.as_ref()?.algorithm_from_wire_id(wire_id)? { + Algorithm::Rsa1024 => 0x06, + Algorithm::Rsa2048 => 0x07, + Algorithm::Rsa3072 => 0x05, + Algorithm::Rsa4096 => 0x16, + Algorithm::EccP256 => 0x11, + Algorithm::EccP384 => 0x14, + Algorithm::EccP521 => 0x15, + Algorithm::Secp256k1 => 0x53, + Algorithm::Sm2 => 0x54, + Algorithm::Ed25519 => 0xe0, + Algorithm::X25519 => 0xe1, + Algorithm::MlDsa65 => 0xe2, + Algorithm::MlKem768 => 0xe3, + }, + ) + } + + /// Idempotent local cleanup. Does not alter the card or already-created ops. + #[flutter_rust_bridge::frb(sync)] + pub fn close(&mut self) { + self.inner.take(); + } +} + +fn openpgp_slot(slot: u8) -> Result { + match slot { + 0 => Ok(openpgp::Slot::Signature), + 1 => Ok(openpgp::Slot::Decryption), + 2 => Ok(openpgp::Slot::Authentication), + _ => Err(Error::new(ErrorKind::InvalidArgument)), + } +} + +fn piv_slot(reference: u8) -> Result { + match reference { + 0x9a => Ok(piv::Slot::Authentication), + 0x9c => Ok(piv::Slot::Signature), + 0x9d => Ok(piv::Slot::KeyManagement), + 0x9e => Ok(piv::Slot::CardAuthentication), + 0x82..=0x95 => piv::RetiredSlot::new(reference - 0x81).map(piv::Slot::Retired), + _ => Err(Error::new(ErrorKind::InvalidArgument)), + } +} + +// The pinned high-level get_pin_status inserts SELECT. This selected-only +// adapter uses its upstream command and error classifier, preserving live PIN +// verification. It returns only the validated status word, never response data. +/// A single caller-owned operation, retained across Dart await points. +/// Close is idempotent and releases Rust buffers without sending any APDU. +#[flutter_rust_bridge::frb(opaque)] +pub struct ProtocolOperation { + // FRB opaque handles require Sync, while core operations are only Send. + // All entry points take &mut self; get_mut never locks or spans Dart I/O. + inner: Mutex>, + construction_error: Option, +} + +impl ProtocolOperation { + /// Admin-only discovery before authentication; does not select PIV. + #[flutter_rust_bridge::frb(sync)] + pub fn probe_admin() -> Self { + Self::from_operation( + canokey::probe_device(canokey::ProbeOptions { + mode: canokey::ProbeMode::Minimal, + ..Default::default() + }) + .map(Inner::Probe), + ) + } + + /// One raw bootstrap identity step for connection setup. The caller's + /// exclusive lease spans SELECT and the serial read; a final non-9000 + /// status fails with the raw status word retained. No retry or replay. + #[flutter_rust_bridge::frb(sync)] + pub fn bootstrap_identity(step: BootstrapIdentityStep) -> Self { + let options = OperationOptions::default(); + let operation = match step { + BootstrapIdentityStep::SelectAdmin => { + conversation(admin::command::select(), options).map(Inner::BootstrapSelect) + } + BootstrapIdentityStep::Serial => { + conversation(admin::command::serial(), options).map(Inner::BootstrapSerial) + } + }; + Self::from_operation(operation) + } + + /// Retained upstream progress, available even after protocol failure. + #[flutter_rust_bridge::frb(sync)] + pub fn admin_progress(&mut self) -> Option { + match self + .inner + .get_mut() + .unwrap_or_else(|e| e.into_inner()) + .as_ref()? + { + Inner::Admin(op) => op.progress().map(admin_progress), + _ => None, + } + } + + /// Explicit Admin/PIV discovery. Call before authentication in the same + /// exclusive lease; the completed profile is transferred to Dart exactly once. + #[flutter_rust_bridge::frb(sync)] + pub fn probe_piv() -> Self { + Self::from_operation( + canokey::probe_device(canokey::ProbeOptions::default()).map(Inner::Probe), + ) + } + + /// Construct without I/O. Only Select changes applet selection; reads never + /// probe, SELECT, authenticate, or retain a device profile implicitly. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_read(kind: PivReadOperation) -> Self { + let options = OperationOptions::default(); + let operation = match kind { + PivReadOperation::PinStatus => { + piv::get_pin_status_selected(options).map(Inner::PinStatus) + } + PivReadOperation::Select => piv::select_application(options).map(Inner::Bytes), + PivReadOperation::Version => piv::read_version_selected(options).map(Inner::Bytes), + PivReadOperation::AlgorithmConfiguration => { + piv::read_configuration_selected(options).map(Inner::Configuration) + } + }; + Self::from_operation(operation) + } + + /// Profile-free NDEF capability-container read. Selects the NDEF applet. + /// Data encodes `max_message_length` (big-endian u16, excluding the two + /// NLEN bytes) followed by the read-only flag byte. + #[flutter_rust_bridge::frb(sync)] + pub fn ndef_read_capability() -> Self { + Self::from_operation( + ndef::read_capability(OperationOptions::default()).map(Inner::NdefCapability), + ) + } + + /// Profile-free full NDEF message read; chunked by the upstream machine. + /// Data is the raw message without the two NLEN bytes. + #[flutter_rust_bridge::frb(sync)] + pub fn ndef_read_message() -> Self { + Self::from_operation( + ndef::read_message(OperationOptions::default()).map(Inner::NdefMessage), + ) + } + + /// Crash-safe NDEF message replacement: zero NLEN, message chunks, real + /// NLEN. A failed mid-write leaves no stale message and is never replayed. + #[flutter_rust_bridge::frb(sync)] + pub fn ndef_write_message(message: Vec) -> Self { + Self::from_operation( + ndef::write_message(&message, OperationOptions::default()).map(Inner::Management), + ) + } + + /// Select the FIDO2 applet only; the caller owns the selected context. + #[flutter_rust_bridge::frb(sync)] + pub fn ctap_select_application() -> Self { + Self::from_operation( + ctap::select_application(OperationOptions::default()).map(Inner::Management), + ) + } + + /// One CTAP message to the caller's already selected FIDO2 applet. + /// Data is the CTAP status byte followed by the payload; a non-success + /// status is not a protocol error and stays visible to Dart. + #[flutter_rust_bridge::frb(sync)] + pub fn ctap_transceive_selected(message: Vec) -> Self { + Self::from_operation( + ctap::transceive_selected(&message, OperationOptions::default()).map(Inner::Ctap), + ) + } + + /// Select the FIDO2 applet and send one CTAP message within that selection. + #[flutter_rust_bridge::frb(sync)] + pub fn ctap_transceive(message: Vec) -> Self { + Self::from_operation( + ctap::transceive(&message, OperationOptions::default()).map(Inner::Ctap), + ) + } + + fn from_operation(operation: Result) -> Self { + match operation { + Ok(inner) => Self { + inner: Mutex::new(Some(inner)), + construction_error: None, + }, + Err(error) => Self { + inner: Mutex::new(None), + construction_error: Some(error), + }, + } + } + + /// Start exactly once; subsequent calls report an operation-state error. + #[flutter_rust_bridge::frb(sync)] + pub fn start(&mut self) -> ProtocolStep { + self.drive(None) + } + + /// Consume one complete, unprocessed response including SW1/SW2. + /// The input is copied into zeroizing storage and released before returning. + #[flutter_rust_bridge::frb(sync)] + pub fn advance(&mut self, response: Vec) -> ProtocolStep { + let response = SecretBytes::new(response); + self.drive(Some(response.as_bytes())) + } + + /// Release locally, including on a transport exception. Never retry/rollback. + #[flutter_rust_bridge::frb(sync)] + pub fn close(&mut self) { + self.inner + .get_mut() + .unwrap_or_else(|e| e.into_inner()) + .take(); + self.construction_error.take(); + } +} + +impl ProtocolOperation { + fn drive(&mut self, response: Option<&[u8]>) -> ProtocolStep { + if let Some(error) = self.construction_error.take() { + return ProtocolStep::failure(error); + } + let result = match self + .inner + .get_mut() + .unwrap_or_else(|e| e.into_inner()) + .as_mut() + { + Some(Inner::Admin(op)) => drive(op, response, |outcome| ProtocolStep { + command: None, + data: None, + profile: None, + error: None, + admin: Some(admin_result(outcome)), + }), + Some(Inner::Bytes(op)) => drive(op, response, |data| bytes(data.as_bytes().to_vec())), + Some(Inner::PinStatus(op)) => drive(op, response, |status| { + let sw = if status.blocked { + 0x6983 + } else if let Some(retries) = status.retries_remaining { + 0x63c0 | u16::from(retries) + } else { + 0x9000 + }; + bytes(sw.to_be_bytes().to_vec()) + }), + Some(Inner::Configuration(op)) => { + drive(op, response, |data| bytes(data.raw().to_vec())) + } + Some(Inner::Certificate(op)) => drive(op, response, |data| bytes(data.der().to_vec())), + Some(Inner::Credential(op)) => drive(op, response, |_| bytes(Vec::new())), + Some(Inner::Management(op)) => drive(op, response, |_| bytes(Vec::new())), + Some(Inner::BootstrapSelect(op)) => drive(op, response, |outcome| { + match outcome.ensure_success(Phase::Select) { + Ok(()) => bytes(Vec::new()), + Err(error) => ProtocolStep::failure(error), + } + }), + Some(Inner::BootstrapSerial(op)) => drive(op, response, |outcome| { + match outcome.ensure_success(Phase::Command) { + Ok(()) => bytes(outcome.data.as_bytes().to_vec()), + Err(error) => ProtocolStep::failure(error), + } + }), + Some(Inner::Metadata(op)) => drive(op, response, |data| { + bytes(data.fields().raw.as_bytes().to_vec()) + }), + Some(Inner::Directory(op)) => drive(op, response, |data| bytes(data.raw().to_vec())), + Some(Inner::PublicKey(op)) => drive(op, response, |data| match data.to_spki_der() { + Ok(value) => bytes(value), + Err(error) => ProtocolStep::failure(error), + }), + Some(Inner::Signature(op)) => { + drive(op, response, |data| bytes(data.as_bytes().to_vec())) + } + Some(Inner::Sm2Agreement(op)) => { + drive(op, response, |data| bytes(data.key.as_bytes().to_vec())) + } + Some(Inner::Oath(op)) => drive(op, response, |outcome| match outcome { + oath::Outcome::Unit => bytes(Vec::new()), + oath::Outcome::Entries(entries) => { + let mut out = Vec::new(); + for entry in entries { + out.push(entry.algorithm_type); + out.push(entry.name.as_bytes().len() as u8); + out.extend_from_slice(entry.name.as_bytes()); + } + bytes(out) + } + oath::Outcome::Selection(selection) => { + let mut out = selection.version.to_vec(); + out.extend_from_slice(&selection.handle); + if let Some(challenge) = selection.challenge { + out.extend_from_slice(&challenge); + } + bytes(out) + } + oath::Outcome::LegacySelection { serial } => { + bytes(serial.map(|s| s.to_vec()).unwrap_or_default()) + } + oath::Outcome::Calculations(calculations) => { + // name_len (0xff = nameless) | name | digits | code tag | + // code_len | code bytes. Raw truncated bytes are preserved + // so Dart owns display formatting (decimal, Steam, etc.). + let mut out = Vec::new(); + for calculation in calculations { + match &calculation.name { + Some(name) => { + out.push(name.as_bytes().len() as u8); + out.extend_from_slice(name.as_bytes()); + } + None => out.push(0xff), + } + out.push(calculation.digits); + match &calculation.code { + oath::Code::Truncated(raw) => { + out.push(0x76); + out.push(raw.as_bytes().len() as u8); + out.extend_from_slice(raw.as_bytes()); + } + oath::Code::Full(raw) => { + out.push(0x75); + out.push(raw.as_bytes().len() as u8); + out.extend_from_slice(raw.as_bytes()); + } + oath::Code::Hotp => out.extend_from_slice(&[0x77, 0]), + oath::Code::TouchRequired => out.extend_from_slice(&[0x7c, 0]), + } + } + bytes(out) + } + }), + Some(Inner::OpenPgp(op)) => drive(op, response, |outcome| match outcome { + openpgp::Outcome::Bytes(value) => bytes(value.as_bytes().to_vec()), + openpgp::Outcome::Unit => bytes(Vec::new()), + openpgp::Outcome::PinStatus(status) => bytes(vec![ + u8::from(status.verified), + u8::from(status.blocked), + status.retries_remaining.unwrap_or(0), + ]), + _ => ProtocolStep::failure(Error::new(ErrorKind::InvalidResponse)), + }), + Some(Inner::Probe(op)) => drive(op, response, |profile| ProtocolStep { + command: None, + data: None, + error: None, + admin: None, + profile: Some(ProtocolProfile { + inner: Some(profile), + }), + }), + Some(Inner::NdefCapability(op)) => drive(op, response, |capability| { + let max = capability.max_message_length.min(usize::from(u16::MAX)) as u16; + let mut data = max.to_be_bytes().to_vec(); + data.push(u8::from(capability.read_only)); + bytes(data) + }), + Some(Inner::NdefMessage(op)) => { + drive(op, response, |message| bytes(message.as_bytes().to_vec())) + } + Some(Inner::Ctap(op)) => drive(op, response, |result| { + let mut data = Vec::with_capacity(result.payload().len() + 1); + data.push(result.status().raw()); + data.extend_from_slice(result.payload()); + bytes(data) + }), + None => Err(Error::new(ErrorKind::OperationStateError)), + }; + result.unwrap_or_else(ProtocolStep::failure) + } +} + +/// Both halves of an OATH access proof travel together; a key without a fresh +/// challenge (or vice versa) is rejected before any I/O. +fn oath_access( + key: Option>, + challenge: Option>, +) -> Result, Error> { + match (key, challenge) { + (None, None) => Ok(None), + (Some(key), Some(challenge)) => Ok(Some(oath::Access { + key: oath::AccessKey::from_bytes(&key)?, + challenge: challenge + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?, + })), + _ => Err(Error::new(ErrorKind::InvalidArgument)), + } +} + +fn drive( + op: &mut Operation, + response: Option<&[u8]>, + encode: impl FnOnce(T) -> ProtocolStep, +) -> Result { + let step = match response { + Some(response) => op.advance(response)?, + None => op.start()?, + }; + Ok(match step { + Step::Exchange => ProtocolStep { + command: Some(op.command()?.as_bytes().to_vec()), + data: None, + error: None, + profile: None, + admin: None, + }, + Step::Done => encode(op.take_result()?), + }) +} + +fn bytes(data: Vec) -> ProtocolStep { + ProtocolStep { + command: None, + data: Some(data), + error: None, + profile: None, + admin: None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn metadata_profile() -> ProtocolProfile { + let mut observations = canokey::compatibility::DeviceObservations::new(b"3.1.0".to_vec()); + observations.piv_version = Some(canokey::compatibility::PivApplicationVersion([6, 0, 0])); + ProtocolProfile { + inner: Some(canokey::DeviceProfile::from_observations(observations).unwrap()), + } + } + + #[test] + fn admin_progress_survives_failure_until_close() { + let profile = metadata_profile(); + let mut op = profile.admin_configure( + AdminConfigurationPatch { + led_on: Some(false), + ndef_read_only: None, + ndef_enabled: Some(false), + webusb_landing: None, + feature_mask: 0, + feature_values: 0, + }, + Some(b"123456".to_vec()), + false, + ); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0] + ); + assert!(!op.admin_progress().unwrap().reprobe_required); + op.advance(vec![0x90, 0]); // VERIFY + op.advance(vec![0x90, 0]); // READ CONFIG + assert_eq!( + op.advance(vec![1, 0, 0, 1, 1, 63, 0x90, 0]) + .command + .unwrap(), + [0, 0x40, 1, 0] + ); + assert!(op.admin_progress().unwrap().reprobe_required); + assert_eq!(op.admin_progress().unwrap().confirmed_writes, 0); + assert_eq!(op.advance(vec![0x90, 0]).command.unwrap(), [0, 0x40, 4, 0]); + assert_eq!( + op.advance(vec![0x6f, 0]).error.unwrap().kind, + "UnexpectedStatusWord" + ); + assert_eq!(op.admin_progress().unwrap().confirmed_writes, 1); + assert!(op.admin_progress().unwrap().reprobe_required); + op.close(); + assert!(op.admin_progress().is_none()); + } + + #[test] + fn admin_operation_copies_profile_and_does_not_accept_cached_authorization() { + let mut profile = metadata_profile(); + let mut reset = profile.admin_action(AdminAction::ResetNdef, None, vec![], 0, 0, false); + assert_eq!( + reset.start().error.unwrap().kind, + "SecurityStatusNotSatisfied" + ); + let mut verify = profile.admin_action( + AdminAction::VerifyPin, + Some(b"123456".to_vec()), + vec![], + 0, + 0, + false, + ); + profile.close(); + verify.start(); + assert_eq!( + verify.advance(vec![0x90, 0]).command.unwrap(), + b"\x00\x20\x00\x00\x06123456" + ); + let error = verify.advance(vec![0x63, 0xc2]).error.unwrap(); + assert_eq!(error.reference.as_deref(), Some("AdminPin")); + assert_eq!(error.retries_remaining, Some(2)); + assert_eq!(verify.admin_progress().unwrap().confirmed_writes, 0); + } + + #[test] + fn credentials_own_inputs_and_preserve_failure_reference_without_replay() { + for (kind, prefix, reference) in [ + (PivCredentialOperation::ChangePin, [0, 0x24, 0, 0x80], "Pin"), + (PivCredentialOperation::ChangePuk, [0, 0x24, 0, 0x81], "Puk"), + ( + PivCredentialOperation::UnblockPin, + [0, 0x2c, 0, 0x80], + "Puk", + ), + ] { + let mut profile = metadata_profile(); + let mut op = profile.piv_credential(kind, b"12345678".to_vec(), b"654321".to_vec()); + profile.close(); + let command = op.start().command.unwrap(); + assert_eq!(&command[..4], &prefix); + assert_eq!(&command[5..13], b"12345678"); + assert_eq!(&command[13..], b"654321\xff\xff"); + let error = op.advance(vec![0x63, 0xc2]).error.unwrap(); + assert_eq!(error.kind, "AuthenticationFailed"); + assert_eq!(error.reference.as_deref(), Some(reference)); + assert_eq!(error.retries_remaining, Some(2)); + assert!(op.advance(vec![0x90, 0]).error.is_some()); + } + for response in [vec![0x6c, 0], vec![1, 0x90, 0]] { + let mut op = metadata_profile().piv_credential( + PivCredentialOperation::ChangePin, + b"123456".to_vec(), + b"654321".to_vec(), + ); + op.start(); + let step = op.advance(response); + assert!(step.command.is_none()); + assert!(step.error.is_some()); + } + } + + #[test] + fn credentials_validate_before_io_and_keep_legacy_le() { + let profile = metadata_profile(); + for input in [vec![], b"123456789".to_vec(), vec![0xff; 6]] { + let mut op = profile.piv_credential(PivCredentialOperation::VerifyPin, input, vec![]); + assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); + } + let mut observations = canokey::compatibility::DeviceObservations::new(b"1.3".to_vec()); + observations.piv_version = Some(canokey::compatibility::PivApplicationVersion([5, 7, 0])); + let profile = ProtocolProfile { + inner: Some(canokey::DeviceProfile::from_observations(observations).unwrap()), + }; + let mut op = profile.piv_credential( + PivCredentialOperation::VerifyPin, + b"123456".to_vec(), + vec![], + ); + assert_eq!( + op.start().command.unwrap(), + b"\x00\x20\x00\x80\x08123456\xff\xff\x00" + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + } + + #[test] + fn selected_pin_status_never_selects_and_validates_acknowledgment() { + for status in [0x9000u16, 0x63c0, 0x63c3, 0x6983] { + let mut op = ProtocolOperation::piv_read(PivReadOperation::PinStatus); + assert_eq!(op.start().command.unwrap(), [0, 0x20, 0, 0x80, 0]); + assert_eq!( + op.advance(status.to_be_bytes().to_vec()).data.unwrap(), + status.to_be_bytes() + ); + } + for response in [vec![1, 0x90, 0], vec![0x6d, 0], vec![0x90]] { + let mut op = ProtocolOperation::piv_read(PivReadOperation::PinStatus); + op.start(); + assert!(op.advance(response).error.is_some()); + } + } + + #[test] + fn probe_transfers_profile_and_metadata_copies_its_evidence() { + let mut probe = ProtocolOperation::probe_piv(); + assert_eq!( + probe.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0, 0] + ); + let mut step = probe.advance(vec![0x90, 0]); + for response in [ + b"3.1.0\x90\0".to_vec(), + b"CanoKey\x90\0".to_vec(), + vec![1, 2, 3, 4, 0x90, 0], + vec![0x90, 0], + vec![6, 0, 0, 0x90, 0], + vec![0x6d, 0], + ] { + assert!(step.command.is_some()); + step = probe.advance(response); + } + let mut profile = step.profile.unwrap(); + probe.close(); + let mut metadata = profile.piv_metadata(0x80); + profile.close(); + profile.close(); + assert_eq!( + profile.piv_metadata(0x80).start().error.unwrap().kind, + "OperationStateError" + ); + assert_eq!(metadata.start().command.unwrap(), [0, 0xf7, 0, 0x80, 0]); + assert_eq!( + metadata + .advance(vec![1, 1, 0xff, 6, 2, 3, 2, 0x90, 0]) + .data + .unwrap(), + [1, 1, 0xff, 6, 2, 3, 2] + ); + } + + #[test] + fn metadata_keeps_card_errors_and_rejects_duplicate_fields() { + let profile = metadata_profile(); + for (response, kind) in [ + (vec![0x6a, 0x88], "NotFound"), + (vec![0x69, 0x82], "SecurityStatusNotSatisfied"), + (vec![1, 1, 0xff, 1, 1, 0xff, 0x90, 0], "InvalidResponse"), + ] { + let mut metadata = profile.piv_metadata(0x80); + assert_eq!(metadata.start().command.unwrap(), [0, 0xf7, 0, 0x80, 0]); + assert_eq!(metadata.advance(response).error.unwrap().kind, kind); + assert_eq!(metadata.start().error.unwrap().kind, "OperationStateError"); + } + assert_eq!( + profile.piv_metadata(0x96).start().error.unwrap().kind, + "InvalidArgument" + ); + } + + #[test] + fn certificate_read_corrects_le_and_unwraps_without_select() { + let mut op = metadata_profile().piv_certificate(5); + assert_eq!( + op.start().command.unwrap(), + [0, 0xcb, 0x3f, 0xff, 5, 0x5c, 3, 0x5f, 0xc1, 5, 0] + ); + assert_eq!(op.advance(vec![0x6c, 7]).command.unwrap().last(), Some(&7)); + assert_eq!( + op.advance(vec![0x53, 5, 0x70, 3, 0x61, 3]).command.unwrap(), + [0, 0xc0, 0, 0, 3] + ); + assert_eq!( + op.advance(vec![0x30, 1, 0, 0x90, 0]).data.unwrap(), + [0x30, 1, 0] + ); + op.close(); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + } + + #[test] + fn certificate_parse_and_card_failures_are_terminal() { + // Preserve the pinned upstream phase, including its known outer-container + // classification bug. The fix belongs to canokey-piv, not this bridge. + for (response, kind, phase) in [ + (vec![0x6a, 0x88], "NotFound", "Command"), + (vec![0x70, 1, 0, 0x90, 0], "InvalidResponse", "Parsing"), + ( + vec![0x53, 3, 0x70, 1, 0, 0x53, 0, 0x90, 0], + "InvalidResponse", + "Parsing", + ), + ] { + let mut op = metadata_profile().piv_certificate(5); + op.start(); + let error = op.advance(response).error.unwrap(); + assert_eq!(error.kind, kind); + assert_eq!(error.phase, phase); + assert_eq!( + op.advance(vec![0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + } + } + + #[test] + fn selected_version_continuation_and_le_correction() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); + assert_eq!(op.start().command.unwrap(), [0, 0xfd, 0, 0, 0]); + assert_eq!( + op.advance(vec![0x6c, 3]).command.unwrap(), + [0, 0xfd, 0, 0, 3] + ); + assert_eq!( + op.advance(vec![6, 0x61, 2]).command.unwrap(), + [0, 0xc0, 0, 0, 2] + ); + assert_eq!(op.advance(vec![0, 0, 0x90, 0]).data.unwrap(), [6, 0, 0]); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + op.close(); + op.close(); + assert_eq!( + op.advance(vec![0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + } + + #[test] + fn select_and_structured_failure() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Select); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xa0, 0, 0, 3, 8, 0] + ); + let error = op.advance(vec![0x6a, 0x82]).error.unwrap(); + assert_eq!(error.kind, "UnsupportedDevice"); + assert_eq!(error.phase, "Select"); + assert_eq!(error.status_word, Some(0x6a82)); + } + + #[test] + fn malformed_and_oversized_responses_are_terminal() { + for response in [vec![0x90], vec![0, 0x90, 0], vec![0; 259]] { + let mut op = ProtocolOperation::piv_read(PivReadOperation::Version); + op.start(); + assert!(op.advance(response).error.is_some()); + assert_eq!( + op.advance(vec![6, 0, 0, 0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + } + } + + #[test] + fn bootstrap_identity_uses_upstream_builders_and_keeps_status_failures() { + let mut select = ProtocolOperation::bootstrap_identity(BootstrapIdentityStep::SelectAdmin); + assert_eq!( + select.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0] + ); + let error = select.advance(vec![0x6a, 0x82]).error.unwrap(); + assert_eq!(error.phase, "Select"); + assert_eq!(error.status_word, Some(0x6a82)); + assert_eq!( + select.advance(vec![0x90, 0]).error.unwrap().kind, + "OperationStateError" + ); + + let mut serial = ProtocolOperation::bootstrap_identity(BootstrapIdentityStep::Serial); + assert_eq!(serial.start().command.unwrap(), [0, 0x32, 0, 0, 0]); + // The upstream builder permits one safe Le correction. + assert_eq!( + serial.advance(vec![0x6c, 4]).command.unwrap(), + [0, 0x32, 0, 0, 4] + ); + assert_eq!( + serial.advance(vec![1, 2, 3, 4, 0x90, 0]).data.unwrap(), + [1, 2, 3, 4] + ); + + let mut serial = ProtocolOperation::bootstrap_identity(BootstrapIdentityStep::Serial); + serial.start(); + let error = serial.advance(vec![0x6d, 0]).error.unwrap(); + assert_eq!(error.status_word, Some(0x6d00)); + serial.close(); + serial.close(); + } + + #[test] + fn configuration_uses_upstream_validation() { + let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + assert_eq!(op.start().command.unwrap(), [0, 0xee, 1, 0, 0]); + assert!(op.advance(vec![1, 0x90, 0]).error.is_some()); + let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + op.start(); + let bytes = vec![1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3]; + let mut response = bytes.clone(); + response.extend([0x90, 0]); + assert_eq!(op.advance(response).data.unwrap(), bytes); + } + + fn oath_selection(protected_: bool) -> Vec { + let mut data = vec![0x79, 3, 6, 0, 0, 0x71, 8]; + data.extend(b"12345678"); + if protected_ { + data.extend([0x74, 8]); + data.extend(b"CCCCCCCC"); + data.extend([0x7b, 1, 1]); + } + data.extend([0x90, 0]); + data + } + + #[test] + fn oath_operation_validates_supplied_access_before_target() { + let profile = metadata_profile(); + let mut op = profile.oath_calculate( + b"test".to_vec(), + 1, + 1, + None, + 0, + Some(b"KKKKKKKKKKKKKKKK".to_vec()), + Some(b"HHHHHHHH".to_vec()), + ); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 7, 0xa0, 0, 0, 5, 0x27, 0x21, 1] + ); + let mut validate = vec![0, 0xa3, 0, 0, 32, 0x75, 20]; + validate.extend([ + 0x0d, 0xe0, 0xba, 0xe2, 0x81, 0xba, 0x21, 0x98, 0x0e, 0x76, 0x92, 0xc9, 0x34, 0x52, + 0x9f, 0x0f, 0x61, 0x11, 0x16, 0x51, + ]); + validate.extend([0x74, 8]); + validate.extend(b"HHHHHHHH"); + assert_eq!(op.advance(oath_selection(true)).command.unwrap(), validate); + let mut proof = vec![0x75, 20]; + proof.extend([ + 0xca, 0xa1, 0x34, 0x6e, 0x39, 0x05, 0x8d, 0xd3, 0x6e, 0xd7, 0x6f, 0xb4, 0x90, 0x53, + 0xe1, 0x4f, 0x66, 0xce, 0x42, 0x8b, + ]); + proof.extend([0x90, 0]); + assert_eq!( + op.advance(proof).command.unwrap(), + [0, 0xa2, 0, 1, 6, 0x71, 4, b't', b'e', b's', b't'] + ); + // Nameless individual calculation: raw truncated bytes stay host-formattable. + assert_eq!( + op.advance(vec![0x76, 5, 6, 0, 0, 0, 42, 0x90, 0]) + .data + .unwrap(), + [0xff, 6, 0x76, 4, 0, 0, 0, 42] + ); + } + + #[test] + fn oath_protected_applet_rejects_missing_or_partial_access_before_io() { + let profile = metadata_profile(); + let mut op = profile.oath_delete(b"test".to_vec(), None, None); + op.start(); + let error = op.advance(oath_selection(true)).error.unwrap(); + assert_eq!(error.kind, "SecurityStatusNotSatisfied"); + assert_eq!(error.phase, "Authentication"); + + let mut op = + profile.oath_delete(b"test".to_vec(), Some(b"KKKKKKKKKKKKKKKK".to_vec()), None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // A rejected access key keeps its credential reference. + let mut op = profile.oath_delete( + b"test".to_vec(), + Some(b"KKKKKKKKKKKKKKKK".to_vec()), + Some(b"HHHHHHHH".to_vec()), + ); + op.start(); + op.advance(oath_selection(true)); + let error = op.advance(vec![0x6a, 0x80]).error.unwrap(); + assert_eq!(error.kind, "AuthenticationFailed"); + assert_eq!(error.phase, "Authentication"); + assert_eq!(error.status_word, Some(0x6a80)); + assert_eq!(error.reference.as_deref(), Some("OathAccess")); + } + + #[test] + fn oath_calculate_all_encodes_names_digits_and_markers() { + let profile = metadata_profile(); + let mut op = profile.oath_calculate_all(vec![0, 0, 0, 0, 0, 0, 0, 1], 0, None, None); + op.start(); + let command = op.advance(oath_selection(false)).command.unwrap(); + assert_eq!(&command[..5], [0, 0xa4, 0, 1, 10]); + let mut page = vec![ + 0x71, 1, b'A', 0x76, 5, 6, 0, 0, 0, 1, // truncated code + 0x71, 1, b'B', 0x77, 1, 6, // HOTP marker + 0x71, 1, b'C', 0x7c, 1, 6, // touch marker + ]; + page.extend([0x90, 0]); + let probe = op.advance(page).command.unwrap(); + assert_eq!(probe, [0, 0xa5, 0, 0, 0xff]); + assert_eq!( + op.advance(vec![0x69, 0x85]).data.unwrap(), + [ + 1, b'A', 6, 0x76, 4, 0, 0, 0, 1, // + 1, b'B', 6, 0x77, 0, // + 1, b'C', 6, 0x7c, 0, + ] + ); + } + + #[test] + fn oath_legacy_dialect_and_modern_capability_gate() { + let legacy = ProtocolProfile { + inner: Some( + canokey::DeviceProfile::from_observations( + canokey::compatibility::DeviceObservations::new(b"1.3.0".to_vec()), + ) + .unwrap(), + ), + }; + // Legacy 1.3 has no access codes: supplying one fails before any I/O. + let mut op = legacy.oath_delete( + b"test".to_vec(), + Some(b"KKKKKKKKKKKKKKKK".to_vec()), + Some(b"HHHHHHHH".to_vec()), + ); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + + let mut op = legacy.oath_delete(b"test".to_vec(), None, None); + op.start(); + let command = op.advance(vec![0x90, 0]).command.unwrap(); + assert_eq!(&command[..4], [0, 2, 0, 0]); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + } + + fn openpgp_profile(firmware: &[u8]) -> ProtocolProfile { + ProtocolProfile { + inner: Some( + canokey::DeviceProfile::from_observations( + canokey::compatibility::DeviceObservations::new(firmware.to_vec()), + ) + .unwrap(), + ), + } + } + + /// Drive SELECT + explicit VERIFY PW3(12345678) + target, asserting wire bytes. + fn drive_openpgp_write(mut op: ProtocolOperation, target: &[u8]) -> ProtocolOperation { + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 6, 0xd2, 0x76, 0, 1, 0x24, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + b"\x00\x20\x00\x83\x0812345678" + ); + assert_eq!(op.advance(vec![0x90, 0]).command.unwrap(), target); + op + } + + #[test] + fn openpgp_admin_writes_select_verify_and_own_wire_bytes() { + let profile = metadata_profile(); + let mut op = drive_openpgp_write( + profile.openpgp_write_reset_code(Some(b"12345678".to_vec()), b"12345678".to_vec()), + b"\x00\xda\x00\xd3\x0812345678", + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // None clears the reset code with a dataless PUT DATA. + let mut op = drive_openpgp_write( + profile.openpgp_write_reset_code(None, b"12345678".to_vec()), + &[0, 0xda, 0, 0xd3], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + let mut op = drive_openpgp_write( + profile.openpgp_reset_retries(vec![3, 4, 5], b"12345678".to_vec()), + &[0, 0xf2, 0, 0, 3, 3, 4, 5], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + let mut op = drive_openpgp_write( + profile.openpgp_write_signature_pin_policy(false, b"12345678".to_vec()), + &[0, 0xda, 0, 0xc4, 1, 0], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + let mut op = drive_openpgp_write( + profile.openpgp_write_signature_pin_policy(true, b"12345678".to_vec()), + &[0, 0xda, 0, 0xc4, 1, 1], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + let mut op = drive_openpgp_write( + profile.openpgp_unblock_with_admin(b"654321".to_vec(), b"12345678".to_vec()), + b"\x00\x2c\x02\x81\x06654321", + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + let mut op = drive_openpgp_write( + profile.openpgp_write_touch_policy(1, 1, b"12345678".to_vec()), + &[0, 0xda, 0, 0xd7, 2, 1, 0x20], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + let mut op = drive_openpgp_write( + profile.openpgp_write_touch_cache_time(15, b"12345678".to_vec()), + &[0, 0xda, 1, 2, 1, 15], + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + } + + #[test] + fn openpgp_unblock_with_code_skips_verify_and_maps_reset_code_failures() { + let profile = metadata_profile(); + for (sw, kind, retries) in [ + (0x63c2u16, "AuthenticationFailed", Some(2)), + (0x6983, "PinBlocked", None), + ] { + let mut op = + profile.openpgp_unblock_with_code(b"24682468".to_vec(), b"654321".to_vec()); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 6, 0xd2, 0x76, 0, 1, 0x24, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + b"\x00\x2c\x00\x81\x0e24682468654321" + ); + let error = op.advance(sw.to_be_bytes().to_vec()).error.unwrap(); + assert_eq!(error.kind, kind); + assert_eq!(error.phase, "Authentication"); + assert_eq!(error.reference.as_deref(), Some("ResetCode")); + assert_eq!(error.retries_remaining, retries); + } + let mut op = profile.openpgp_unblock_with_code(b"24682468".to_vec(), b"654321".to_vec()); + op.start(); + op.advance(vec![0x90, 0]); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + } + + #[test] + fn openpgp_write_failures_keep_credential_reference_and_status() { + let profile = metadata_profile(); + // VERIFY rejection: reference, retries and phase are retained. + let mut op = + profile.openpgp_write_reset_code(Some(b"12345678".to_vec()), b"12345678".to_vec()); + op.start(); + op.advance(vec![0x90, 0]); + let error = op.advance(vec![0x63, 0xc2]).error.unwrap(); + assert_eq!(error.kind, "AuthenticationFailed"); + assert_eq!(error.phase, "Authentication"); + assert_eq!(error.reference.as_deref(), Some("Pw3")); + assert_eq!(error.retries_remaining, Some(2)); + + // OpenPGP VERIFY 6982 means authentication failure without a retry count. + let mut op = profile.openpgp_reset_retries(vec![3, 3, 3], b"12345678".to_vec()); + op.start(); + op.advance(vec![0x90, 0]); + let error = op.advance(vec![0x69, 0x82]).error.unwrap(); + assert_eq!(error.kind, "AuthenticationFailed"); + assert_eq!(error.reference.as_deref(), Some("Pw3")); + assert_eq!(error.retries_remaining, None); + + // A security rejection at the target stage carries no credential reference. + let mut op = profile.openpgp_write_touch_cache_time(15, b"12345678".to_vec()); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + let error = op.advance(vec![0x69, 0x82]).error.unwrap(); + assert_eq!(error.kind, "SecurityStatusNotSatisfied"); + assert_eq!(error.phase, "Command"); + assert_eq!(error.reference, None); + } + + #[test] + fn openpgp_bridge_validates_arguments_and_capabilities_before_io() { + // Reset code must satisfy the 8..64 administrative length. + for code in [b"123456".to_vec(), vec![b'x'; 65]] { + let mut op = + metadata_profile().openpgp_write_reset_code(Some(code), b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); + } + // Retry limits are exactly three values in 1..=15. + let mut op = metadata_profile().openpgp_reset_retries(vec![3, 3], b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = metadata_profile().openpgp_reset_retries(vec![0, 3, 3], b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // Touch policy and slot identifiers are validated in the bridge. + let mut op = metadata_profile().openpgp_write_touch_policy(0, 3, b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = metadata_profile().openpgp_write_touch_policy(3, 1, b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // Reset codes below the administrative length fail before any I/O. + let mut op = + metadata_profile().openpgp_unblock_with_code(b"123456".to_vec(), b"654321".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); + + // UIF writes require the 1.5.2 capability; retry reset requires 3.1.0. + let legacy = openpgp_profile(b"1.3.0"); + let mut op = legacy.openpgp_write_touch_policy(0, 1, b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + let mut op = legacy.openpgp_write_touch_cache_time(15, b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + let pre31 = openpgp_profile(b"1.6.0"); + let mut op = pre31.openpgp_reset_retries(vec![3, 3, 3], b"12345678".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + } + + /// 3.1.0 with the observed default extension IDs (Ed25519 E0, SM2 54, + /// ML-DSA-65 E2), so streaming modes resolve their wire IDs. + fn streaming_profile(firmware: &[u8]) -> ProtocolProfile { + let mut observations = canokey::compatibility::DeviceObservations::new(firmware.to_vec()); + observations.piv_version = Some(canokey::compatibility::PivApplicationVersion([6, 0, 0])); + observations.algorithm_config = Some( + canokey::compatibility::AlgorithmConfig::parse(&[ + 1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3, + ]) + .unwrap(), + ); + ProtocolProfile { + inner: Some(canokey::DeviceProfile::from_observations(observations).unwrap()), + } + } + + #[test] + fn attest_keeps_wire_bytes_and_maps_card_failures() { + let mut op = metadata_profile().piv_attest(0x9a); + assert_eq!(op.start().command.unwrap(), [0, 0xf9, 0x9a, 0, 0]); + assert_eq!( + op.advance(vec![0x30, 3, 2, 1, 5, 0x90, 0]).data.unwrap(), + [0x30, 3, 2, 1, 5] + ); + + // An empty payload is malformed, never an empty certificate. + let mut op = metadata_profile().piv_attest(0x9a); + op.start(); + assert_eq!( + op.advance(vec![0x90, 0]).error.unwrap().kind, + "InvalidResponse" + ); + + // Card failures retain status word and phase; nothing is replayed. + let mut op = metadata_profile().piv_attest(0x9a); + op.start(); + let error = op.advance(vec![0x6a, 0x80]).error.unwrap(); + assert_eq!(error.kind, "UnexpectedStatusWord"); + assert_eq!(error.phase, "Command"); + assert_eq!(error.status_word, Some(0x6a80)); + assert!(op.advance(vec![0x90, 0]).error.is_some()); + + // Attestation is gated to evidenced 3.1.0 firmware before any I/O. + let mut observations = canokey::compatibility::DeviceObservations::new(b"3.0.3".to_vec()); + observations.piv_version = Some(canokey::compatibility::PivApplicationVersion([6, 0, 0])); + let legacy = ProtocolProfile { + inner: Some(canokey::DeviceProfile::from_observations(observations).unwrap()), + }; + let mut op = legacy.piv_attest(0x9a); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + } + + #[test] + fn streaming_sign_owns_wire_bytes_and_result_validation() { + // ML-DSA-65: raw message in 7C{82 00, 81 message} under the observed ID. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 0, b"abc".to_vec(), None); + assert_eq!( + op.start().command.unwrap(), + b"\x00\x87\xe2\x9a\x09\x7c\x07\x82\x00\x81\x03abc" + ); + let signature = vec![7u8; 3309]; + let mut payload = vec![0x7c, 0x82, 0x0c, 0xf1, 0x82, 0x82, 0x0c, 0xed]; + payload.extend(&signature); + // Replies beyond one physical exchange continue through GET RESPONSE. + let mut rest = payload.as_slice(); + let mut step = op.advance([&rest[..256], &[0x61, 0]].concat()); + rest = &rest[256..]; + while let Some(command) = step.command { + assert_eq!(&command[..4], &[0, 0xc0, 0, 0]); + let take = rest.len().min(256); + let mut chunk = rest[..take].to_vec(); + rest = &rest[take..]; + if rest.is_empty() { + chunk.extend([0x90, 0]); + } else { + chunk.extend([0x61, u8::try_from(rest.len()).unwrap_or(0)]); + } + step = op.advance(chunk); + } + assert_eq!(step.data.unwrap(), signature); + + // A wrong-length ML-DSA reply is malformed, not a signature. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 0, b"abc".to_vec(), None); + op.start(); + let mut short = vec![0x7c, 4, 0x82, 2, 1, 2]; + short.extend([0x90, 0]); + assert_eq!(op.advance(short).error.unwrap().kind, "InvalidResponse"); + + // Randomized Ed25519 always uses wire mode FF; empty messages sign. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 1, vec![], None); + assert_eq!( + op.start().command.unwrap(), + [0, 0x87, 0xff, 0x9a, 6, 0x7c, 4, 0x82, 0, 0x81, 0] + ); + let mut response = vec![0x7c, 0x42, 0x82, 0x40]; + response.extend([3u8; 64]); + response.extend([0x90, 0]); + assert_eq!(op.advance(response).data.unwrap(), [3u8; 64]); + + // SM2: a chained prefix carrying only the outer tag precedes the body, + // and the result stays in the card's P1363 encoding. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 2, b"abc".to_vec(), None); + assert_eq!( + op.start().command.unwrap(), + [0x10, 0x87, 0x54, 0x9a, 1, 0x7c] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + b"\x00\x87\x54\x9a\x08\x07\x82\x00\x81\x03abc" + ); + // Nonempty intermediate data violates the prefix acknowledgment. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 2, b"abc".to_vec(), None); + op.start(); + assert!(op.advance(vec![1, 0x90, 0]).error.is_some()); + } + + #[test] + fn streaming_sign_validates_mode_and_capabilities_before_io() { + // Unknown modes and invalid slots fail without a command. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 3, vec![1], None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x80, 0, vec![1], None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // An SM2 user ID outside 1..=32 bytes is rejected before any I/O. + let mut op = streaming_profile(b"3.1.0").piv_sign_streaming(0x9a, 2, vec![1], Some(vec![])); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // Streaming modes require evidenced 3.1.0 firmware and observed + // enabled algorithm IDs; older firmware and unobserved IDs fail fast. + for mode in [0, 1, 2] { + let mut op = streaming_profile(b"3.0.3").piv_sign_streaming(0x9a, mode, vec![1], None); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + } + let mut op = metadata_profile().piv_sign_streaming(0x9a, 0, vec![1], None); + assert!(op.start().error.is_some()); + } + + #[test] + fn pass_slots_read_encodes_typed_states_and_keeps_unknown_types() { + let profile = metadata_profile(); + let mut op = profile.admin_pass_slots(Some(b"123456".to_vec()), false); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + b"\x00\x20\x00\x00\x06123456" + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0x43, 0, 0, 0] + ); + // Short = static with enter; long = OATH credential "test" without enter. + let mut raw = vec![0x02, 0x01, 0x01, 4]; + raw.extend(b"test"); + raw.extend([0, 0x90, 0]); + let result = op.advance(raw).admin.unwrap(); + assert!(matches!(result.kind, AdminValueKind::PassSlots)); + assert_eq!( + result.data, + [2, 0x02, 0x01, 7, 0x01, 4, b't', b'e', b's', b't', 0] + ); + + // Unknown type bytes pass through verbatim; off stays a single byte. + let mut op = profile.admin_pass_slots(Some(b"123456".to_vec()), false); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + let result = op.advance(vec![0x7f, 0x00, 0x90, 0]).admin.unwrap(); + assert_eq!(result.data, [1, 0x7f, 1, 0x00]); + + // Malformed dumps remain parsing failures, never fabricated slots. + let mut op = profile.admin_pass_slots(None, true); + assert_eq!(op.start().command.unwrap(), [0, 0x43, 0, 0, 0]); + let error = op.advance(vec![0x02, 0x90, 0]).error.unwrap(); + assert_eq!(error.kind, "InvalidResponse"); + assert_eq!(error.phase, "Parsing"); + } + + #[test] + fn admin_existing_never_selects_or_verifies_and_rejects_pin() { + let profile = metadata_profile(); + // Existing plus a PIN is meaningless and fails before any I/O. + let mut op = profile.admin_pass_slots(Some(b"123456".to_vec()), true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // VerifyPin carries no PIN of its own and is rejected under Existing. + let mut op = profile.admin_operation(Ok(admin::Request::VerifyPin), None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // A protected write under Existing is exactly its target command. + let mut op = profile.admin_set_pass_slot(0, 1, b"pw".to_vec(), true, None, true); + assert_eq!( + op.start().command.unwrap(), + [0, 0x44, 1, 0, 5, 2, 2, b'p', b'w', 1] + ); + let result = op.advance(vec![0x90, 0]).admin.unwrap(); + assert!(matches!(result.kind, AdminValueKind::None)); + assert_eq!(result.progress.confirmed_writes, 1); + assert!(result.progress.reprobe_required); + } + + #[test] + fn admin_read_configure_and_action_honor_existing_access() { + let profile = metadata_profile(); + // Existing skips SELECT on reads and rejects a carried PIN preflight. + let mut op = profile.admin_read(AdminReadOperation::ChipId, None, true); + assert_eq!(op.start().command.unwrap(), [0, 0x32, 1, 0, 0]); + let mut op = profile.admin_read(AdminReadOperation::ChipId, Some(b"123456".to_vec()), true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // An Existing configure is only the validated read/patch sequence. + let mut op = profile.admin_configure( + AdminConfigurationPatch { + led_on: Some(false), + ndef_read_only: None, + ndef_enabled: None, + webusb_landing: None, + feature_mask: 0, + feature_values: 0, + }, + None, + true, + ); + assert_eq!(op.start().command.unwrap(), [0, 0x42, 0, 0, 0]); + assert_eq!( + op.advance(vec![1, 0, 0, 1, 1, 63, 0x90, 0]) + .command + .unwrap(), + [0, 0x40, 1, 0] + ); + assert!(op.advance(vec![0x90, 0]).admin.is_some()); + let mut op = profile.admin_configure( + AdminConfigurationPatch { + led_on: None, + ndef_read_only: None, + ndef_enabled: None, + webusb_landing: None, + feature_mask: 0, + feature_values: 0, + }, + Some(b"123456".to_vec()), + true, + ); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // An Existing write action is exactly its target command; VerifyPin + // under Existing is rejected by the pinned upstream contract. + let mut op = profile.admin_action(AdminAction::Nfc, None, vec![], 0, 0, true); + assert_eq!(op.start().command.unwrap(), [0, 0x14, 1, 0]); + assert!(op.advance(vec![0x90, 0]).admin.is_some()); + let mut op = profile.admin_action( + AdminAction::VerifyPin, + Some(b"123456".to_vec()), + vec![], + 0, + 0, + true, + ); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = profile.admin_action(AdminAction::VerifyPin, None, vec![], 0, 0, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + } + + #[test] + fn set_pass_slot_validates_slot_kind_and_secret_before_io() { + let profile = metadata_profile(); + let mut op = profile.admin_set_pass_slot(2, 1, b"pw".to_vec(), false, None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = profile.admin_set_pass_slot(0, 3, vec![], false, None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // Static passwords are at most 32 printable ASCII bytes. + let mut op = profile.admin_set_pass_slot(0, 1, vec![b'x'; 33], false, None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = profile.admin_set_pass_slot(0, 1, vec![0x7f], false, None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // HMAC-SHA1 keys are exactly twenty bytes. + let mut op = profile.admin_set_pass_slot(1, 2, vec![0; 19], false, None, true); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + // Without Existing or a PIN the protected write fails preflight. + let mut op = profile.admin_set_pass_slot(0, 0, vec![], false, None, false); + assert_eq!(op.start().error.unwrap().kind, "SecurityStatusNotSatisfied"); + + // HMAC-SHA1 long-slot wire bytes with the per-request PIN sequence. + let mut op = + profile.admin_set_pass_slot(1, 2, vec![9; 20], false, Some(b"123456".to_vec()), false); + op.start(); + op.advance(vec![0x90, 0]); + let mut expected = vec![0, 0x44, 2, 0, 22, 3, 0x14]; + expected.extend([9; 20]); + assert_eq!(op.advance(vec![0x90, 0]).command.unwrap(), expected); + let result = op.advance(vec![0x90, 0]).admin.unwrap(); + assert!(matches!(result.kind, AdminValueKind::None)); + assert!(result.progress.reprobe_required); + } + + #[test] + fn oath_set_default_uses_profile_dialect_and_validates_before_io() { + let profile = metadata_profile(); + let mut op = profile.oath_set_default(1, true, "test".to_string(), None, None); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 7, 0xa0, 0, 0, 5, 0x27, 0x21, 1] + ); + assert_eq!( + op.advance(oath_selection(false)).command.unwrap(), + [0, 0x55, 2, 1, 6, 0x71, 4, b't', b'e', b's', b't'] + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // Unknown slots fail before any I/O. + let mut op = profile.oath_set_default(2, false, "test".to_string(), None, None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // Legacy firmware has one slot and no enter flag; only Short/false + // fits its zero-P1/P2 dialect. Long or append-enter fail fast. + let legacy = openpgp_profile(b"1.3.0"); + let mut op = legacy.oath_set_default(1, false, "test".to_string(), None, None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = legacy.oath_set_default(0, true, "test".to_string(), None, None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = legacy.oath_set_default(0, false, "test".to_string(), None, None); + op.start(); + // The legacy dialect carries an explicit trailing Le. + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0x55, 0, 0, 6, 0x71, 4, b't', b'e', b's', b't', 0] + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + } + + #[test] + fn oath_set_default_validates_access_like_other_writes() { + let profile = metadata_profile(); + let mut op = profile.oath_set_default( + 0, + false, + "test".to_string(), + Some(b"KKKKKKKKKKKKKKKK".to_vec()), + Some(b"HHHHHHHH".to_vec()), + ); + op.start(); + let mut validate = vec![0, 0xa3, 0, 0, 32, 0x75, 20]; + validate.extend([ + 0x0d, 0xe0, 0xba, 0xe2, 0x81, 0xba, 0x21, 0x98, 0x0e, 0x76, 0x92, 0xc9, 0x34, 0x52, + 0x9f, 0x0f, 0x61, 0x11, 0x16, 0x51, + ]); + validate.extend([0x74, 8]); + validate.extend(b"HHHHHHHH"); + assert_eq!(op.advance(oath_selection(true)).command.unwrap(), validate); + let mut proof = vec![0x75, 20]; + proof.extend([ + 0xca, 0xa1, 0x34, 0x6e, 0x39, 0x05, 0x8d, 0xd3, 0x6e, 0xd7, 0x6f, 0xb4, 0x90, 0x53, + 0xe1, 0x4f, 0x66, 0xce, 0x42, 0x8b, + ]); + proof.extend([0x90, 0]); + assert_eq!( + op.advance(proof).command.unwrap(), + [0, 0x55, 1, 0, 6, 0x71, 4, b't', b'e', b's', b't'] + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // A missing challenge half is rejected before any I/O. + let mut op = profile.oath_set_default( + 0, + false, + "test".to_string(), + Some(b"KKKKKKKKKKKKKKKK".to_vec()), + None, + ); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + } + + #[test] + fn pq_seed_import_owns_seed_tlv_and_capability_gate() { + // ML-DSA-65: INS FE under the observed E2 wire ID with the 09 seed TLV. + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x9a, 0, vec![7; 32], 0, 0); + let mut expected = vec![0, 0xfe, 0xe2, 0x9a, 34, 0x09, 0x20]; + expected.extend([7; 32]); + assert_eq!(op.start().command.unwrap(), expected); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // ML-KEM-768: 64-byte d||z seed under the 0A TLV and E3 wire ID. + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x9d, 1, vec![8; 64], 0, 0); + let mut expected = vec![0, 0xfe, 0xe3, 0x9d, 66, 0x0a, 0x40]; + expected.extend([8; 64]); + assert_eq!(op.start().command.unwrap(), expected); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // Seed length, kind and slot are validated before any I/O. + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x9a, 0, vec![7; 31], 0, 0); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x9a, 1, vec![8; 32], 0, 0); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x9a, 2, vec![7; 32], 0, 0); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = streaming_profile(b"3.1.0").piv_import_pq_seed(0x96, 0, vec![7; 32], 0, 0); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + + // PQ algorithms require evidenced support; unobserved IDs fail fast. + let mut op = metadata_profile().piv_import_pq_seed(0x9a, 0, vec![7; 32], 0, 0); + assert!(op.start().error.is_some()); + } + + #[test] + fn ndef_capability_and_message_reads_own_wire_bytes() { + let cc = |write_access: u8| { + let mut cc = vec![ + 0x00, + 0x0f, + 0x20, + 0x00, + 0xff, + 0x00, + 0xff, + 0x04, + 0x06, + 0xe1, + 0x04, + 0x04, + 0x00, + 0x00, + write_access, + ]; + cc.extend([0x90, 0]); + cc + }; + let mut op = ProtocolOperation::ndef_read_capability(); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 7, 0xd2, 0x76, 0, 0, 0x85, 1, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xa4, 0, 0x0c, 2, 0xe1, 0x03] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xb0, 0, 0, 15] + ); + // Max file size 0x0400 clamps to the 1022-byte message maximum. + assert_eq!(op.advance(cc(0)).data.unwrap(), [0x03, 0xfe, 0]); + let mut op = ProtocolOperation::ndef_read_capability(); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + assert_eq!(op.advance(cc(1)).data.unwrap(), [0x03, 0xfe, 1]); + + // 6A82 during selection is an absent/disabled applet; a malformed CC + // is a parsing failure, never invented limits. + let mut op = ProtocolOperation::ndef_read_capability(); + op.start(); + let error = op.advance(vec![0x6a, 0x82]).error.unwrap(); + assert_eq!(error.kind, "UnsupportedDevice"); + assert_eq!(error.phase, "Select"); + let mut op = ProtocolOperation::ndef_read_capability(); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + let mut malformed_cc = vec![0; 15]; + malformed_cc.extend([0x90, 0]); + let error = op.advance(malformed_cc).error.unwrap(); + assert_eq!(error.kind, "InvalidResponse"); + + // Message read: NLEN then the message from offset two; NLEN zero ends. + let mut op = ProtocolOperation::ndef_read_message(); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + assert_eq!( + op.advance(cc(0)).command.unwrap(), + [0, 0xa4, 0, 0x0c, 2, 0, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xb0, 0, 0, 2] + ); + assert_eq!( + op.advance(vec![0, 3, 0x90, 0]).command.unwrap(), + [0, 0xb0, 0, 2, 3] + ); + assert_eq!(op.advance(vec![1, 2, 3, 0x90, 0]).data.unwrap(), [1, 2, 3]); + } + + #[test] + fn ndef_write_is_crash_safe_three_phase_and_bounded() { + let mut op = ProtocolOperation::ndef_write_message(b"hi".to_vec()); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 7, 0xd2, 0x76, 0, 0, 0x85, 1, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xa4, 0, 0x0c, 2, 0, 1] + ); + // Zero NLEN first: an interrupted write leaves no stale message. + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xd6, 0, 0, 2, 0, 0] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xd6, 0, 2, 2, b'h', b'i'] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xd6, 0, 0, 2, 0, 2] + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + + // A read-only file is the card's 6982, never a host-side guess. + let mut op = ProtocolOperation::ndef_write_message(b"hi".to_vec()); + op.start(); + op.advance(vec![0x90, 0]); + op.advance(vec![0x90, 0]); + let error = op.advance(vec![0x69, 0x82]).error.unwrap(); + assert_eq!(error.kind, "SecurityStatusNotSatisfied"); + + // Messages beyond the firmware maximum fail before any I/O. + let mut op = ProtocolOperation::ndef_write_message(vec![0; 1023]); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + } + + #[test] + fn ctap_transceive_selects_continues_and_preserves_status() { + let mut op = ProtocolOperation::ctap_transceive(vec![4]); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 8, 0xa0, 0, 0, 6, 0x47, 0x2f, 0, 1] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0x80, 0x10, 0, 0, 1, 4] + ); + // ISO7816 continuation uses GET RESPONSE with the CTAP class byte. + assert_eq!( + op.advance(vec![0x61, 2]).command.unwrap(), + [0x80, 0xc0, 0, 0, 2] + ); + // A non-success CTAP status byte is data, not a protocol error. + assert_eq!(op.advance(vec![0x2e, 1, 0x90, 0]).data.unwrap(), [0x2e, 1]); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + + // transceive_selected sends no SELECT of its own; an empty success + // reply without a CTAP status byte is malformed. + let mut op = ProtocolOperation::ctap_transceive_selected(vec![4]); + assert_eq!(op.start().command.unwrap(), [0x80, 0x10, 0, 0, 1, 4]); + let error = op.advance(vec![0x90, 0]).error.unwrap(); + assert_eq!(error.kind, "InvalidResponse"); + + // select_application emits only SELECT; 6A82 is an absent FIDO2 applet. + let mut op = ProtocolOperation::ctap_select_application(); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 8, 0xa0, 0, 0, 6, 0x47, 0x2f, 0, 1] + ); + assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); + let mut op = ProtocolOperation::ctap_select_application(); + op.start(); + let error = op.advance(vec![0x6a, 0x82]).error.unwrap(); + assert_eq!(error.kind, "UnsupportedDevice"); + assert_eq!(error.phase, "Select"); + + // An empty CTAP message fails before any I/O. + let mut op = ProtocolOperation::ctap_transceive(vec![]); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 61959f5..f9190f4 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1621666059; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2116397449; // Section: executor @@ -48,14 +48,14 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs -fn wire__crate__api__protocol__ProtocolOperation_advance_impl( +fn wire__crate__api__protocol__ProtocolOperation_admin_progress_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "ProtocolOperation_advance", + debug_name: "ProtocolOperation_admin_progress", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -72,7 +72,6 @@ fn wire__crate__api__protocol__ProtocolOperation_advance_impl( let api_that = , >>::sse_decode(&mut deserializer); - let api_response = >::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { let mut api_that_guard = None; @@ -89,23 +88,22 @@ fn wire__crate__api__protocol__ProtocolOperation_advance_impl( } } let mut api_that_guard = api_that_guard.unwrap(); - let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::advance( - &mut *api_that_guard, - api_response, - ))?; + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::admin_progress(&mut *api_that_guard), + )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__protocol__ProtocolOperation_close_impl( +fn wire__crate__api__protocol__ProtocolOperation_advance_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "ProtocolOperation_close", + debug_name: "ProtocolOperation_advance", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -122,6 +120,7 @@ fn wire__crate__api__protocol__ProtocolOperation_close_impl( let api_that = , >>::sse_decode(&mut deserializer); + let api_response = >::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { let mut api_that_guard = None; @@ -138,22 +137,23 @@ fn wire__crate__api__protocol__ProtocolOperation_close_impl( } } let mut api_that_guard = api_that_guard.unwrap(); - let output_ok = Ok::<_, ()>({ - crate::api::protocol::ProtocolOperation::close(&mut *api_that_guard); - })?; + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::advance( + &mut *api_that_guard, + api_response, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__protocol__ProtocolOperation_piv_read_impl( +fn wire__crate__api__protocol__ProtocolOperation_bootstrap_identity_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "ProtocolOperation_piv_read", + debug_name: "ProtocolOperation_bootstrap_identity", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -167,24 +167,26 @@ fn wire__crate__api__protocol__ProtocolOperation_piv_read_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_kind = ::sse_decode(&mut deserializer); + let api_step = + ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = - Ok::<_, ()>(crate::api::protocol::ProtocolOperation::piv_read(api_kind))?; + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::bootstrap_identity(api_step), + )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__protocol__ProtocolOperation_start_impl( +fn wire__crate__api__protocol__ProtocolOperation_close_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "ProtocolOperation_start", + debug_name: "ProtocolOperation_close", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -217,22 +219,22 @@ fn wire__crate__api__protocol__ProtocolOperation_start_impl( } } let mut api_that_guard = api_that_guard.unwrap(); - let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::start( - &mut *api_that_guard, - ))?; + let output_ok = Ok::<_, ()>({ + crate::api::protocol::ProtocolOperation::close(&mut *api_that_guard); + })?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__build_piv_public_key_impl( +fn wire__crate__api__protocol__ProtocolOperation_ctap_select_application_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "build_piv_public_key", + debug_name: "ProtocolOperation_ctap_select_application", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -246,29 +248,24 @@ fn wire__crate__api__piv_crypto__build_piv_public_key_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_card_data = >::sse_decode(&mut deserializer); - let api_generated_response = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::build_piv_public_key( - api_algorithm, - api_card_data, - api_generated_response, + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::ctap_select_application(), )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__decode__decode_png_qrcode_impl( +fn wire__crate__api__protocol__ProtocolOperation_ctap_transceive_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "decode_png_qrcode", + debug_name: "ProtocolOperation_ctap_transceive", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -282,23 +279,25 @@ fn wire__crate__api__decode__decode_png_qrcode_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_png_file = >::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::decode::decode_png_qrcode(api_png_file)?; + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::ctap_transceive(api_message), + )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( +fn wire__crate__api__protocol__ProtocolOperation_ctap_transceive_selected_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "encrypt_piv_management_key_challenge", + debug_name: "ProtocolOperation_ctap_transceive_selected", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -312,29 +311,25 @@ fn wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_key = >::sse_decode(&mut deserializer); - let api_challenge = >::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::crypto::encrypt_piv_management_key_challenge( - api_algorithm, - api_key, - api_challenge, + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::ctap_transceive_selected(api_message), )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__finish_piv_csr_impl( +fn wire__crate__api__protocol__ProtocolOperation_ndef_read_capability_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "finish_piv_csr", + debug_name: "ProtocolOperation_ndef_read_capability", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -348,29 +343,23 @@ fn wire__crate__api__piv_crypto__finish_piv_csr_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_certification_request_info = >::sse_decode(&mut deserializer); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_signature = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::finish_piv_csr( - api_certification_request_info, - api_algorithm, - api_signature, - )?; + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::ndef_read_capability())?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( +fn wire__crate__api__protocol__ProtocolOperation_ndef_read_message_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "finish_self_signed_certificate", + debug_name: "ProtocolOperation_ndef_read_message", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -384,29 +373,23 @@ fn wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_tbs_certificate = >::sse_decode(&mut deserializer); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_signature = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::finish_self_signed_certificate( - api_tbs_certificate, - api_algorithm, - api_signature, - )?; + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::ndef_read_message())?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__hmac_sha1_impl( +fn wire__crate__api__protocol__ProtocolOperation_ndef_write_message_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "hmac_sha1", + debug_name: "ProtocolOperation_ndef_write_message", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -420,24 +403,25 @@ fn wire__crate__api__crypto__hmac_sha1_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_key = >::sse_decode(&mut deserializer); - let api_data = >::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::hmac_sha1(api_key, api_data))?; + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::ndef_write_message(api_message), + )?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__init_app_impl( +fn wire__crate__api__protocol__ProtocolOperation_piv_read_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "init_app", + debug_name: "ProtocolOperation_piv_read", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -451,24 +435,24 @@ fn wire__crate__api__init_app_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_kind = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>({ - crate::api::init_app(); - })?; + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::piv_read(api_kind))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__parse_piv_import_file_impl( +fn wire__crate__api__protocol__ProtocolOperation_probe_admin_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_piv_import_file", + debug_name: "ProtocolOperation_probe_admin", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -482,23 +466,23 @@ fn wire__crate__api__piv_crypto__parse_piv_import_file_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_bytes = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::parse_piv_import_file(api_bytes)?; + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_admin())?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( +fn wire__crate__api__protocol__ProtocolOperation_probe_piv_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_piv_public_key_info", + debug_name: "ProtocolOperation_probe_piv", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -512,27 +496,22 @@ fn wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_subject_public_key_info = >::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::parse_piv_public_key_info( - api_algorithm, - api_subject_public_key_info, - )?; + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_piv())?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__parse_x509_cert_from_der_impl( +fn wire__crate__api__protocol__ProtocolOperation_start_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_x509_cert_from_der", + debug_name: "ProtocolOperation_start", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -546,23 +525,41 @@ fn wire__crate__api__crypto__parse_x509_cert_from_der_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_der = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::crypto::parse_x509_cert_from_der(api_der)?; + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::start( + &mut *api_that_guard, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__parse_x509_cert_from_pem_impl( +fn wire__crate__api__protocol__ProtocolProfile_admin_action_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "parse_x509_cert_from_pem", + debug_name: "ProtocolProfile_admin_action", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -576,23 +573,53 @@ fn wire__crate__api__crypto__parse_x509_cert_from_pem_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_pem = ::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_kind = ::sse_decode(&mut deserializer); + let api_pin = >>::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + let api_index = ::sse_decode(&mut deserializer); + let api_value = ::sse_decode(&mut deserializer); + let api_existing = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::crypto::parse_x509_cert_from_pem(api_pem)?; + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::admin_action( + &*api_that_guard, + api_kind, + api_pin, + api_data, + api_index, + api_value, + api_existing, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__pbkdf2_hmac_sha1_impl( +fn wire__crate__api__protocol__ProtocolProfile_admin_configure_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "pbkdf2_hmac_sha1", + debug_name: "ProtocolProfile_admin_configure", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -606,31 +633,49 @@ fn wire__crate__api__crypto__pbkdf2_hmac_sha1_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_password = ::sse_decode(&mut deserializer); - let api_salt = >::sse_decode(&mut deserializer); - let api_iterations = ::sse_decode(&mut deserializer); - let api_key_len = ::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_patch = + ::sse_decode(&mut deserializer); + let api_pin = >>::sse_decode(&mut deserializer); + let api_existing = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::pbkdf2_hmac_sha1( - api_password, - api_salt, - api_iterations, - api_key_len, - ))?; + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::admin_configure( + &*api_that_guard, + api_patch, + api_pin, + api_existing, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( +fn wire__crate__api__protocol__ProtocolProfile_admin_pass_slots_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "piv_certificate_supports_macos", + debug_name: "ProtocolProfile_admin_pass_slots", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -644,32 +689,46 @@ fn wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_der = >::sse_decode(&mut deserializer); - let api_expected_public_key = >::sse_decode(&mut deserializer); - let api_slot = ::sse_decode(&mut deserializer); - let api_now_unix = ::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_pin = >>::sse_decode(&mut deserializer); + let api_existing = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); let output_ok = - Ok::<_, ()>(crate::api::piv_crypto::piv_certificate_supports_macos( - api_der, - api_expected_public_key, - api_slot, - api_now_unix, + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::admin_pass_slots( + &*api_that_guard, + api_pin, + api_existing, ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__prepare_piv_csr_impl( +fn wire__crate__api__protocol__ProtocolProfile_admin_read_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "prepare_piv_csr", + debug_name: "ProtocolProfile_admin_read", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -683,35 +742,48 @@ fn wire__crate__api__piv_crypto__prepare_piv_csr_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_common_name = ::sse_decode(&mut deserializer); - let api_organization = >::sse_decode(&mut deserializer); - let api_organizational_unit = >::sse_decode(&mut deserializer); - let api_country = >::sse_decode(&mut deserializer); - let api_subject_public_key_info = >::sse_decode(&mut deserializer); - let api_subject_alternative_names = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_kind = + ::sse_decode(&mut deserializer); + let api_pin = >>::sse_decode(&mut deserializer); + let api_existing = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::prepare_piv_csr( - api_common_name, - api_organization, - api_organizational_unit, - api_country, - api_subject_public_key_info, - api_subject_alternative_names, - )?; - std::result::Result::Ok(output_ok) - })()) - }, - ) -} -fn wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "prepare_piv_signing_input", + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::admin_read( + &*api_that_guard, + api_kind, + api_pin, + api_existing, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_admin_set_pass_slot_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_admin_set_pass_slot", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -725,29 +797,54 @@ fn wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_algorithm = ::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_kind = ::sse_decode(&mut deserializer); let api_data = >::sse_decode(&mut deserializer); - let api_public_key = >>::sse_decode(&mut deserializer); + let api_append_enter = ::sse_decode(&mut deserializer); + let api_pin = >>::sse_decode(&mut deserializer); + let api_existing = ::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::piv_crypto::prepare_piv_signing_input( - api_algorithm, - api_data, - api_public_key, - )?; + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::admin_set_pass_slot( + &*api_that_guard, + api_slot, + api_kind, + api_data, + api_append_enter, + api_pin, + api_existing, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( +fn wire__crate__api__protocol__ProtocolProfile_close_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "prepare_self_signed_certificate", + debug_name: "ProtocolProfile_close", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -761,26 +858,41 @@ fn wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_params = ::sse_decode( - &mut deserializer, - ); + let api_that = , + >>::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = - crate::api::piv_crypto::prepare_self_signed_certificate(api_params)?; + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::protocol::ProtocolProfile::close(&mut *api_that_guard); + })?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__sha256_digest_impl( +fn wire__crate__api__protocol__ProtocolProfile_firmware_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "sha256_digest", + debug_name: "ProtocolProfile_firmware", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -794,23 +906,41 @@ fn wire__crate__api__crypto__sha256_digest_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::sha256_digest(api_data))?; + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::firmware( + &*api_that_guard, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__sha384_digest_impl( +fn wire__crate__api__protocol__ProtocolProfile_model_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "sha384_digest", + debug_name: "ProtocolProfile_model", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -824,23 +954,41 @@ fn wire__crate__api__crypto__sha384_digest_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::sha384_digest(api_data))?; + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::model( + &*api_that_guard, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__sha512_digest_impl( +fn wire__crate__api__protocol__ProtocolProfile_oath_calculate_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "sha512_digest", + debug_name: "ProtocolProfile_oath_calculate", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -854,23 +1002,56 @@ fn wire__crate__api__crypto__sha512_digest_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = >::sse_decode(&mut deserializer); + let api_kind = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_challenge = >>::sse_decode(&mut deserializer); + let api_format = ::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::sha512_digest(api_data))?; + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_calculate( + &*api_that_guard, + api_name, + api_kind, + api_algorithm, + api_challenge, + api_format, + api_access_key, + api_access_challenge, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__sm2_message_digest_impl( +fn wire__crate__api__protocol__ProtocolProfile_oath_calculate_all_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "sm2_message_digest", + debug_name: "ProtocolProfile_oath_calculate_all", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -884,24 +1065,50 @@ fn wire__crate__api__crypto__sm2_message_digest_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_data = >::sse_decode(&mut deserializer); - let api_public_key = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_challenge = >::sse_decode(&mut deserializer); + let api_format = ::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); deserializer.end(); - transform_result_sse::<_, String>((move || { - let output_ok = crate::api::crypto::sm2_message_digest(api_data, api_public_key)?; + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_calculate_all( + &*api_that_guard, + api_challenge, + api_format, + api_access_key, + api_access_challenge, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__tdes_ede3_enc_impl( +fn wire__crate__api__protocol__ProtocolProfile_oath_clear_code_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "tdes_ede3_enc", + debug_name: "ProtocolProfile_oath_clear_code", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -915,24 +1122,46 @@ fn wire__crate__api__crypto__tdes_ede3_enc_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_key = >::sse_decode(&mut deserializer); - let api_data = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_key = >>::sse_decode(&mut deserializer); + let api_challenge = >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::tdes_ede3_enc(api_key, api_data))?; + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_clear_code( + &*api_that_guard, + api_key, + api_challenge, + ))?; std::result::Result::Ok(output_ok) })()) }, ) } -fn wire__crate__api__crypto__verify_piv_signature_impl( +fn wire__crate__api__protocol__ProtocolProfile_oath_delete_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, data_len_: i32, ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "verify_piv_signature", + debug_name: "ProtocolProfile_oath_delete", port: None, mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, }, @@ -946,466 +1175,5120 @@ fn wire__crate__api__crypto__verify_piv_signature_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_algorithm = ::sse_decode(&mut deserializer); - let api_public_key = >::sse_decode(&mut deserializer); - let api_data = >::sse_decode(&mut deserializer); - let api_signature = >::sse_decode(&mut deserializer); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = >::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::crypto::verify_piv_signature( - api_algorithm, - api_public_key, - api_data, - api_signature, + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_delete( + &*api_that_guard, + api_name, + api_access_key, + api_access_challenge, ))?; std::result::Result::Ok(output_ok) })()) }, ) } - -// Section: related_funcs - -flutter_rust_bridge::frb_generated_moi_arc_impl_value!( - flutter_rust_bridge::for_generated::RustAutoOpaqueInner -); - -// Section: dart2rust - -impl SseDecode for ProtocolOperation { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = , - >>::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); - } -} - -impl SseDecode - for RustOpaqueMoi> -{ - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return decode_rust_opaque_moi(inner); - } -} - -impl SseDecode for String { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = >::sse_decode(deserializer); - return String::from_utf8(inner).unwrap(); - } -} - -impl SseDecode for bool { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() != 0 - } -} - -impl SseDecode for i32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i32::().unwrap() - } -} - -impl SseDecode for i64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i64::().unwrap() - } +fn wire__crate__api__protocol__ProtocolProfile_oath_list_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_list", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_list( + &*api_that_guard, + api_access_key, + api_access_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) } - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } +fn wire__crate__api__protocol__ProtocolProfile_oath_put_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_put", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = >::sse_decode(&mut deserializer); + let api_kind = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_digits = ::sse_decode(&mut deserializer); + let api_secret = >::sse_decode(&mut deserializer); + let api_require_touch = ::sse_decode(&mut deserializer); + let api_increasing = ::sse_decode(&mut deserializer); + let api_initial_counter = ::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_put( + &*api_that_guard, + api_name, + api_kind, + api_algorithm, + api_digits, + api_secret, + api_require_touch, + api_increasing, + api_initial_counter, + api_access_key, + api_access_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) } - -impl SseDecode for Vec { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut len_ = ::sse_decode(deserializer); - let mut ans_ = Vec::with_capacity(len_ as usize); - for idx_ in 0..len_ { - ans_.push(::sse_decode(deserializer)); - } - return ans_; - } +fn wire__crate__api__protocol__ProtocolProfile_oath_rename_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_rename", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_old = >::sse_decode(&mut deserializer); + let api_new = >::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_rename( + &*api_that_guard, + api_old, + api_new, + api_access_key, + api_access_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) } - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } +fn wire__crate__api__protocol__ProtocolProfile_oath_select_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_select", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_select( + &*api_that_guard, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) } - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } - } +fn wire__crate__api__protocol__ProtocolProfile_oath_set_code_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_set_code", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_old_key = >>::sse_decode(&mut deserializer); + let api_new_key = >::sse_decode(&mut deserializer); + let api_challenge = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_set_code( + &*api_that_guard, + api_old_key, + api_new_key, + api_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) } - -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } +fn wire__crate__api__protocol__ProtocolProfile_oath_set_default_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_set_default", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_append_enter = ::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_access_key = >>::sse_decode(&mut deserializer); + let api_access_challenge = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_set_default( + &*api_that_guard, + api_slot, + api_append_enter, + api_name, + api_access_key, + api_access_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_oath_validate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_oath_validate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_key = >::sse_decode(&mut deserializer); + let api_challenge = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::oath_validate( + &*api_that_guard, + api_key, + api_challenge, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_activate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_activate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_activate( + &*api_that_guard, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_change_password_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_change_password", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reference = ::sse_decode(&mut deserializer); + let api_old = >::sse_decode(&mut deserializer); + let api_new = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_change_password( + &*api_that_guard, + api_reference, + api_old, + api_new, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_generate_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_generate_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_generate_key( + &*api_that_guard, + api_slot, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_logout_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_logout", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reference = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_logout( + &*api_that_guard, + api_reference, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_pin_status_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_pin_status", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reference = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_pin_status( + &*api_that_guard, + api_reference, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_read_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_read_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_read_certificate( + &*api_that_guard, + api_slot, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_read_data_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_read_data", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_tag = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_read_data( + &*api_that_guard, + api_tag, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_reset_retries_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_reset_retries", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_retries = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_reset_retries( + &*api_that_guard, + api_retries, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_terminate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_terminate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_terminate( + &*api_that_guard, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_admin_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_unblock_with_admin", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_new_pin = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_unblock_with_admin( + &*api_that_guard, + api_new_pin, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_code_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_unblock_with_code", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_code = >::sse_decode(&mut deserializer); + let api_new_pin = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_unblock_with_code( + &*api_that_guard, + api_code, + api_new_pin, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_verify_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_verify", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reference = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_verify( + &*api_that_guard, + api_reference, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_certificate = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_certificate( + &*api_that_guard, + api_slot, + api_certificate, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_language_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_language", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_language = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_language( + &*api_that_guard, + api_language, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_login_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_login", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_login = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_write_login( + &*api_that_guard, + api_login, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_name_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_name", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_write_name( + &*api_that_guard, + api_name, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_reset_code_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_reset_code", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reset_code = >>::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_reset_code( + &*api_that_guard, + api_reset_code, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_sex_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_sex", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_sex = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_write_sex( + &*api_that_guard, + api_sex, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_signature_pin_policy_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_signature_pin_policy", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reuse = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_signature_pin_policy( + &*api_that_guard, + api_reuse, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_cache_time_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_touch_cache_time", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_seconds = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_touch_cache_time( + &*api_that_guard, + api_seconds, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_policy_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_touch_policy", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_policy = ::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::openpgp_write_touch_policy( + &*api_that_guard, + api_slot, + api_policy, + api_password, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_openpgp_write_url_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_openpgp_write_url", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_url = >::sse_decode(&mut deserializer); + let api_password = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::openpgp_write_url( + &*api_that_guard, + api_url, + api_password, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_algorithm_display_id_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_algorithm_display_id", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_wire_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_algorithm_display_id( + &*api_that_guard, + api_wire_id, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_attest_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_attest", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_attest( + &*api_that_guard, + api_slot, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_authenticate_management_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_authenticate_management", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_key = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_authenticate_management( + &*api_that_guard, + api_algorithm, + api_key, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_object_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_certificate( + &*api_that_guard, + api_object_id, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_credential_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_credential", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_kind = + ::sse_decode(&mut deserializer); + let api_current = >::sse_decode(&mut deserializer); + let api_replacement = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_credential( + &*api_that_guard, + api_kind, + api_current, + api_replacement, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_decapsulate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_decapsulate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_ciphertext = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_decapsulate( + &*api_that_guard, + api_slot, + api_ciphertext, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_decrypt_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_decrypt", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_ciphertext = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_decrypt( + &*api_that_guard, + api_slot, + api_algorithm, + api_ciphertext, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_delete_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_delete_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_object_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_delete_certificate( + &*api_that_guard, + api_object_id, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_delete_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_delete_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_delete_key( + &*api_that_guard, + api_slot, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_derive_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_derive", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_peer = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_derive( + &*api_that_guard, + api_slot, + api_algorithm, + api_peer, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_generate_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_generate_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_pin_policy = ::sse_decode(&mut deserializer); + let api_touch_policy = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_generate_key( + &*api_that_guard, + api_slot, + api_algorithm, + api_pin_policy, + api_touch_policy, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_import_ec_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_import_ec_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_scalar = >::sse_decode(&mut deserializer); + let api_pin_policy = ::sse_decode(&mut deserializer); + let api_touch_policy = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_import_ec_key( + &*api_that_guard, + api_slot, + api_algorithm, + api_scalar, + api_pin_policy, + api_touch_policy, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_import_ed25519_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_import_ed25519_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_seed = >::sse_decode(&mut deserializer); + let api_pin_policy = ::sse_decode(&mut deserializer); + let api_touch_policy = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_import_ed25519_key( + &*api_that_guard, + api_slot, + api_seed, + api_pin_policy, + api_touch_policy, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_import_pq_seed_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_import_pq_seed", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_kind = ::sse_decode(&mut deserializer); + let api_seed = >::sse_decode(&mut deserializer); + let api_pin_policy = ::sse_decode(&mut deserializer); + let api_touch_policy = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_import_pq_seed( + &*api_that_guard, + api_slot, + api_kind, + api_seed, + api_pin_policy, + api_touch_policy, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_import_rsa_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_import_rsa_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_p = >::sse_decode(&mut deserializer); + let api_q = >::sse_decode(&mut deserializer); + let api_dp = >::sse_decode(&mut deserializer); + let api_dq = >::sse_decode(&mut deserializer); + let api_qinv = >::sse_decode(&mut deserializer); + let api_pin_policy = ::sse_decode(&mut deserializer); + let api_touch_policy = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_import_rsa_key( + &*api_that_guard, + api_slot, + api_algorithm, + api_p, + api_q, + api_dp, + api_dq, + api_qinv, + api_pin_policy, + api_touch_policy, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_metadata_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_metadata", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reference = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_metadata( + &*api_that_guard, + api_reference, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_metadata_directory_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_metadata_directory", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_metadata_directory(&*api_that_guard), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_move_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_move_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_source = ::sse_decode(&mut deserializer); + let api_target = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_move_key( + &*api_that_guard, + api_source, + api_target, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_read_object", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_object_id = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_read_object( + &*api_that_guard, + api_object_id, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_reset_pin_puk_retries", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_pin_retries = ::sse_decode(&mut deserializer); + let api_puk_retries = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_reset_pin_puk_retries( + &*api_that_guard, + api_pin_retries, + api_puk_retries, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_set_algorithm_config", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_raw = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_set_algorithm_config( + &*api_that_guard, + api_raw, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_set_container_name", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_set_container_name( + &*api_that_guard, + api_slot, + api_name, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_set_management_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_key = >::sse_decode(&mut deserializer); + let api_touch = ::sse_decode(&mut deserializer); + let api_update_protected = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_set_management_key( + &*api_that_guard, + api_algorithm, + api_key, + api_touch, + api_update_protected, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_sign_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_sign", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_input = >::sse_decode(&mut deserializer); + let api_input_kind = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_sign( + &*api_that_guard, + api_slot, + api_algorithm, + api_input, + api_input_kind, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_sign_streaming", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_mode = ::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); + let api_user_id = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_sign_streaming( + &*api_that_guard, + api_slot, + api_mode, + api_message, + api_user_id, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_sm2_agreement", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_role = ::sse_decode(&mut deserializer); + let api_peer_static = >::sse_decode(&mut deserializer); + let api_peer_ephemeral = >::sse_decode(&mut deserializer); + let api_user_id = >>::sse_decode(&mut deserializer); + let api_peer_id = >>::sse_decode(&mut deserializer); + let api_key_len = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_sm2_agreement( + &*api_that_guard, + api_slot, + api_role, + api_peer_static, + api_peer_ephemeral, + api_user_id, + api_peer_id, + api_key_len, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_write_object", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_object_id = >::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolProfile::piv_write_object( + &*api_that_guard, + api_object_id, + api_data, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolProfile_serial_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_serial", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolProfile::serial( + &*api_that_guard, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__build_piv_public_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "build_piv_public_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_card_data = >::sse_decode(&mut deserializer); + let api_generated_response = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::build_piv_public_key( + api_algorithm, + api_card_data, + api_generated_response, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__decode__decode_png_qrcode_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "decode_png_qrcode", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_png_file = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::decode::decode_png_qrcode(api_png_file)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "encrypt_piv_management_key_challenge", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_key = >::sse_decode(&mut deserializer); + let api_challenge = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::crypto::encrypt_piv_management_key_challenge( + api_algorithm, + api_key, + api_challenge, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__finish_piv_csr_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "finish_piv_csr", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_certification_request_info = >::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_signature = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::finish_piv_csr( + api_certification_request_info, + api_algorithm, + api_signature, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "finish_self_signed_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_tbs_certificate = >::sse_decode(&mut deserializer); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_signature = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::finish_self_signed_certificate( + api_tbs_certificate, + api_algorithm, + api_signature, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__hmac_sha1_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "hmac_sha1", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_key = >::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::hmac_sha1(api_key, api_data))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__init_app_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init_app", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>({ + crate::api::init_app(); + })?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__parse_piv_import_file_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "parse_piv_import_file", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_bytes = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::parse_piv_import_file(api_bytes)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "parse_piv_public_key_info", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_subject_public_key_info = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::parse_piv_public_key_info( + api_algorithm, + api_subject_public_key_info, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__parse_x509_cert_from_der_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "parse_x509_cert_from_der", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_der = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::crypto::parse_x509_cert_from_der(api_der)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__parse_x509_cert_from_pem_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "parse_x509_cert_from_pem", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_pem = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::crypto::parse_x509_cert_from_pem(api_pem)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__pbkdf2_hmac_sha1_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "pbkdf2_hmac_sha1", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_password = ::sse_decode(&mut deserializer); + let api_salt = >::sse_decode(&mut deserializer); + let api_iterations = ::sse_decode(&mut deserializer); + let api_key_len = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::pbkdf2_hmac_sha1( + api_password, + api_salt, + api_iterations, + api_key_len, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "piv_certificate_supports_macos", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_der = >::sse_decode(&mut deserializer); + let api_expected_public_key = >::sse_decode(&mut deserializer); + let api_slot = ::sse_decode(&mut deserializer); + let api_now_unix = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::piv_crypto::piv_certificate_supports_macos( + api_der, + api_expected_public_key, + api_slot, + api_now_unix, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__prepare_piv_csr_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "prepare_piv_csr", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_common_name = ::sse_decode(&mut deserializer); + let api_organization = >::sse_decode(&mut deserializer); + let api_organizational_unit = >::sse_decode(&mut deserializer); + let api_country = >::sse_decode(&mut deserializer); + let api_subject_public_key_info = >::sse_decode(&mut deserializer); + let api_subject_alternative_names = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::prepare_piv_csr( + api_common_name, + api_organization, + api_organizational_unit, + api_country, + api_subject_public_key_info, + api_subject_alternative_names, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "prepare_piv_signing_input", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + let api_public_key = >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::piv_crypto::prepare_piv_signing_input( + api_algorithm, + api_data, + api_public_key, + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "prepare_self_signed_certificate", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_params = ::sse_decode( + &mut deserializer, + ); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = + crate::api::piv_crypto::prepare_self_signed_certificate(api_params)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__sha256_digest_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sha256_digest", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::sha256_digest(api_data))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__sha384_digest_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sha384_digest", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::sha384_digest(api_data))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__sha512_digest_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sha512_digest", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::sha512_digest(api_data))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__sm2_message_digest_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sm2_message_digest", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_data = >::sse_decode(&mut deserializer); + let api_public_key = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::crypto::sm2_message_digest(api_data, api_public_key)?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__tdes_ede3_enc_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "tdes_ede3_enc", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_key = >::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::tdes_ede3_enc(api_key, api_data))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__crypto__verify_piv_signature_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "verify_piv_signature", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_algorithm = ::sse_decode(&mut deserializer); + let api_public_key = >::sse_decode(&mut deserializer); + let api_data = >::sse_decode(&mut deserializer); + let api_signature = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>(crate::api::crypto::verify_piv_signature( + api_algorithm, + api_public_key, + api_data, + api_signature, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for ProtocolOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for ProtocolProfile { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for crate::api::protocol::AdminAction { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::AdminAction::VerifyPin, + 1 => crate::api::protocol::AdminAction::ChangePin, + 2 => crate::api::protocol::AdminAction::KeyboardInterface, + 3 => crate::api::protocol::AdminAction::KeyboardReturn, + 4 => crate::api::protocol::AdminAction::KeyboardKeymap, + 5 => crate::api::protocol::AdminAction::ClearKeyboardKeymap, + 6 => crate::api::protocol::AdminAction::LegacyPivExtensions, + 7 => crate::api::protocol::AdminAction::LegacyTouch, + 8 => crate::api::protocol::AdminAction::WriteSm2, + 9 => crate::api::protocol::AdminAction::Nfc, + 10 => crate::api::protocol::AdminAction::ResetOpenPgp, + 11 => crate::api::protocol::AdminAction::ResetPiv, + 12 => crate::api::protocol::AdminAction::ResetOath, + 13 => crate::api::protocol::AdminAction::ResetNdef, + 14 => crate::api::protocol::AdminAction::ResetCtap, + 15 => crate::api::protocol::AdminAction::ResetPass, + 16 => crate::api::protocol::AdminAction::FactoryReset, + _ => unreachable!("Invalid variant for AdminAction: {}", inner), + }; + } +} + +impl SseDecode for crate::api::protocol::AdminConfigurationPatch { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_ledOn = >::sse_decode(deserializer); + let mut var_ndefReadOnly = >::sse_decode(deserializer); + let mut var_ndefEnabled = >::sse_decode(deserializer); + let mut var_webusbLanding = >::sse_decode(deserializer); + let mut var_featureMask = ::sse_decode(deserializer); + let mut var_featureValues = ::sse_decode(deserializer); + return crate::api::protocol::AdminConfigurationPatch { + led_on: var_ledOn, + ndef_read_only: var_ndefReadOnly, + ndef_enabled: var_ndefEnabled, + webusb_landing: var_webusbLanding, + feature_mask: var_featureMask, + feature_values: var_featureValues, + }; + } +} + +impl SseDecode for crate::api::protocol::AdminProgress { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_confirmedWrites = ::sse_decode(deserializer); + let mut var_reprobeRequired = ::sse_decode(deserializer); + return crate::api::protocol::AdminProgress { + confirmed_writes: var_confirmedWrites, + reprobe_required: var_reprobeRequired, + }; + } +} + +impl SseDecode for crate::api::protocol::AdminReadOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::AdminReadOperation::Firmware, + 1 => crate::api::protocol::AdminReadOperation::Model, + 2 => crate::api::protocol::AdminReadOperation::Serial, + 3 => crate::api::protocol::AdminReadOperation::ChipId, + 4 => crate::api::protocol::AdminReadOperation::CoreCommit, + 5 => crate::api::protocol::AdminReadOperation::Configuration, + 6 => crate::api::protocol::AdminReadOperation::FlashUsage, + 7 => crate::api::protocol::AdminReadOperation::AppletUsage, + 8 => crate::api::protocol::AdminReadOperation::KeyboardLayout, + 9 => crate::api::protocol::AdminReadOperation::KeyboardKeymap, + 10 => crate::api::protocol::AdminReadOperation::NfcStatus, + 11 => crate::api::protocol::AdminReadOperation::Sm2Configuration, + _ => unreachable!("Invalid variant for AdminReadOperation: {}", inner), + }; + } +} + +impl SseDecode for crate::api::protocol::AdminResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_kind = ::sse_decode(deserializer); + let mut var_data = >::sse_decode(deserializer); + let mut var_progress = ::sse_decode(deserializer); + return crate::api::protocol::AdminResult { + kind: var_kind, + data: var_data, + progress: var_progress, + }; + } +} + +impl SseDecode for crate::api::protocol::AdminValueKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::AdminValueKind::None, + 1 => crate::api::protocol::AdminValueKind::Bytes, + 2 => crate::api::protocol::AdminValueKind::Configuration, + 3 => crate::api::protocol::AdminValueKind::LegacyConfiguration, + 4 => crate::api::protocol::AdminValueKind::FlashUsage, + 5 => crate::api::protocol::AdminValueKind::AppletUsage, + 6 => crate::api::protocol::AdminValueKind::NfcStatus, + 7 => crate::api::protocol::AdminValueKind::Sm2Configuration, + 8 => crate::api::protocol::AdminValueKind::LegacySm2Configuration, + 9 => crate::api::protocol::AdminValueKind::PinStatus, + 10 => crate::api::protocol::AdminValueKind::KeyboardLayout, + 11 => crate::api::protocol::AdminValueKind::KeyboardKeymap, + 12 => crate::api::protocol::AdminValueKind::PassSlots, + _ => unreachable!("Invalid variant for AdminValueKind: {}", inner), + }; + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for crate::api::protocol::BootstrapIdentityStep { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::BootstrapIdentityStep::SelectAdmin, + 1 => crate::api::protocol::BootstrapIdentityStep::Serial, + _ => unreachable!("Invalid variant for BootstrapIdentityStep: {}", inner), + }; + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for i64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i64::().unwrap() + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for crate::api::protocol::PivCredentialOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::PivCredentialOperation::VerifyPin, + 1 => crate::api::protocol::PivCredentialOperation::ChangePin, + 2 => crate::api::protocol::PivCredentialOperation::ChangePuk, + 3 => crate::api::protocol::PivCredentialOperation::UnblockPin, + 4 => crate::api::protocol::PivCredentialOperation::Logout, + _ => unreachable!("Invalid variant for PivCredentialOperation: {}", inner), + }; + } +} + +impl SseDecode for crate::api::piv_crypto::PivImportFileData { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_privateKey = + >::sse_decode(deserializer); + let mut var_certificate = + >::sse_decode(deserializer); + return crate::api::piv_crypto::PivImportFileData { + private_key: var_privateKey, + certificate: var_certificate, + }; + } +} + +impl SseDecode for crate::api::piv_crypto::PivPrivateKeyData { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_algorithm = ::sse_decode(deserializer); + let mut var_importData = >::sse_decode(deserializer); + let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); + return crate::api::piv_crypto::PivPrivateKeyData { + algorithm: var_algorithm, + import_data: var_importData, + subject_public_key_info: var_subjectPublicKeyInfo, + }; + } +} + +impl SseDecode for crate::api::piv_crypto::PivPublicKeyData { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); + let mut var_rawPublicKey = >::sse_decode(deserializer); + return crate::api::piv_crypto::PivPublicKeyData { + subject_public_key_info: var_subjectPublicKeyInfo, + raw_public_key: var_rawPublicKey, + }; + } +} + +impl SseDecode for crate::api::protocol::PivReadOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::protocol::PivReadOperation::Select, + 1 => crate::api::protocol::PivReadOperation::Version, + 2 => crate::api::protocol::PivReadOperation::AlgorithmConfiguration, + 3 => crate::api::protocol::PivReadOperation::PinStatus, + _ => unreachable!("Invalid variant for PivReadOperation: {}", inner), + }; + } +} + +impl SseDecode for crate::api::protocol::ProtocolError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_kind = ::sse_decode(deserializer); + let mut var_phase = ::sse_decode(deserializer); + let mut var_statusWord = >::sse_decode(deserializer); + let mut var_reference = >::sse_decode(deserializer); + let mut var_retriesRemaining = >::sse_decode(deserializer); + return crate::api::protocol::ProtocolError { + kind: var_kind, + phase: var_phase, + status_word: var_statusWord, + reference: var_reference, + retries_remaining: var_retriesRemaining, + }; + } +} + +impl SseDecode for crate::api::protocol::ProtocolStep { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_command = >>::sse_decode(deserializer); + let mut var_data = >>::sse_decode(deserializer); + let mut var_error = >::sse_decode(deserializer); + let mut var_profile = >::sse_decode(deserializer); + let mut var_admin = >::sse_decode(deserializer); + return crate::api::protocol::ProtocolStep { + command: var_command, + data: var_data, + error: var_error, + profile: var_profile, + admin: var_admin, + }; + } +} + +impl SseDecode for crate::api::piv_crypto::SelfSignedCertificateParams { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_commonName = ::sse_decode(deserializer); + let mut var_organization = >::sse_decode(deserializer); + let mut var_organizationalUnit = >::sse_decode(deserializer); + let mut var_country = >::sse_decode(deserializer); + let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); + let mut var_serialNumber = >::sse_decode(deserializer); + let mut var_notBefore = ::sse_decode(deserializer); + let mut var_notAfter = ::sse_decode(deserializer); + let mut var_subjectAlternativeNames = >::sse_decode(deserializer); + let mut var_keyUsage = ::sse_decode(deserializer); + let mut var_keyUsageCritical = ::sse_decode(deserializer); + let mut var_extendedKeyUsage = >::sse_decode(deserializer); + let mut var_includeBasicConstraints = ::sse_decode(deserializer); + return crate::api::piv_crypto::SelfSignedCertificateParams { + common_name: var_commonName, + organization: var_organization, + organizational_unit: var_organizationalUnit, + country: var_country, + subject_public_key_info: var_subjectPublicKeyInfo, + serial_number: var_serialNumber, + not_before: var_notBefore, + not_after: var_notAfter, + subject_alternative_names: var_subjectAlternativeNames, + key_usage: var_keyUsage, + key_usage_critical: var_keyUsageCritical, + extended_key_usage: var_extendedKeyUsage, + include_basic_constraints: var_includeBasicConstraints, + }; + } +} + +impl SseDecode for u16 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u16::().unwrap() + } +} + +impl SseDecode for u32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u32::().unwrap() + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for crate::api::crypto::X509CertData { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_bytes = >::sse_decode(deserializer); + let mut var_subject = ::sse_decode(deserializer); + let mut var_issuer = ::sse_decode(deserializer); + let mut var_notBefore = ::sse_decode(deserializer); + let mut var_notAfter = ::sse_decode(deserializer); + let mut var_serialNumber = ::sse_decode(deserializer); + let mut var_signatureAlgorithm = ::sse_decode(deserializer); + let mut var_signatureValue = >::sse_decode(deserializer); + let mut var_publicKeyAlgorithm = ::sse_decode(deserializer); + let mut var_publicKeySize = ::sse_decode(deserializer); + let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); + let mut var_rawPublicKey = >::sse_decode(deserializer); + return crate::api::crypto::X509CertData { + bytes: var_bytes, + subject: var_subject, + issuer: var_issuer, + not_before: var_notBefore, + not_after: var_notAfter, + serial_number: var_serialNumber, + signature_algorithm: var_signatureAlgorithm, + signature_value: var_signatureValue, + public_key_algorithm: var_publicKeyAlgorithm, + public_key_size: var_publicKeySize, + subject_public_key_info: var_subjectPublicKeyInfo, + raw_public_key: var_rawPublicKey, + }; + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__protocol__ProtocolOperation_admin_progress_impl( + ptr, + rust_vec_len, + data_len, + ), + 2 => { + wire__crate__api__protocol__ProtocolOperation_advance_impl(ptr, rust_vec_len, data_len) + } + 3 => wire__crate__api__protocol__ProtocolOperation_bootstrap_identity_impl( + ptr, + rust_vec_len, + data_len, + ), + 4 => wire__crate__api__protocol__ProtocolOperation_close_impl(ptr, rust_vec_len, data_len), + 5 => wire__crate__api__protocol__ProtocolOperation_ctap_select_application_impl( + ptr, + rust_vec_len, + data_len, + ), + 6 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_impl( + ptr, + rust_vec_len, + data_len, + ), + 7 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_selected_impl( + ptr, + rust_vec_len, + data_len, + ), + 8 => wire__crate__api__protocol__ProtocolOperation_ndef_read_capability_impl( + ptr, + rust_vec_len, + data_len, + ), + 9 => wire__crate__api__protocol__ProtocolOperation_ndef_read_message_impl( + ptr, + rust_vec_len, + data_len, + ), + 10 => wire__crate__api__protocol__ProtocolOperation_ndef_write_message_impl( + ptr, + rust_vec_len, + data_len, + ), + 11 => { + wire__crate__api__protocol__ProtocolOperation_piv_read_impl(ptr, rust_vec_len, data_len) + } + 12 => wire__crate__api__protocol__ProtocolOperation_probe_admin_impl( + ptr, + rust_vec_len, + data_len, + ), + 13 => wire__crate__api__protocol__ProtocolOperation_probe_piv_impl( + ptr, + rust_vec_len, + data_len, + ), + 14 => wire__crate__api__protocol__ProtocolOperation_start_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__protocol__ProtocolProfile_admin_action_impl( + ptr, + rust_vec_len, + data_len, + ), + 16 => wire__crate__api__protocol__ProtocolProfile_admin_configure_impl( + ptr, + rust_vec_len, + data_len, + ), + 17 => wire__crate__api__protocol__ProtocolProfile_admin_pass_slots_impl( + ptr, + rust_vec_len, + data_len, + ), + 18 => { + wire__crate__api__protocol__ProtocolProfile_admin_read_impl(ptr, rust_vec_len, data_len) + } + 19 => wire__crate__api__protocol__ProtocolProfile_admin_set_pass_slot_impl( + ptr, + rust_vec_len, + data_len, + ), + 20 => wire__crate__api__protocol__ProtocolProfile_close_impl(ptr, rust_vec_len, data_len), + 21 => { + wire__crate__api__protocol__ProtocolProfile_firmware_impl(ptr, rust_vec_len, data_len) + } + 22 => wire__crate__api__protocol__ProtocolProfile_model_impl(ptr, rust_vec_len, data_len), + 23 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_impl( + ptr, + rust_vec_len, + data_len, + ), + 24 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_all_impl( + ptr, + rust_vec_len, + data_len, + ), + 25 => wire__crate__api__protocol__ProtocolProfile_oath_clear_code_impl( + ptr, + rust_vec_len, + data_len, + ), + 26 => wire__crate__api__protocol__ProtocolProfile_oath_delete_impl( + ptr, + rust_vec_len, + data_len, + ), + 27 => { + wire__crate__api__protocol__ProtocolProfile_oath_list_impl(ptr, rust_vec_len, data_len) + } + 28 => { + wire__crate__api__protocol__ProtocolProfile_oath_put_impl(ptr, rust_vec_len, data_len) + } + 29 => wire__crate__api__protocol__ProtocolProfile_oath_rename_impl( + ptr, + rust_vec_len, + data_len, + ), + 30 => wire__crate__api__protocol__ProtocolProfile_oath_select_impl( + ptr, + rust_vec_len, + data_len, + ), + 31 => wire__crate__api__protocol__ProtocolProfile_oath_set_code_impl( + ptr, + rust_vec_len, + data_len, + ), + 32 => wire__crate__api__protocol__ProtocolProfile_oath_set_default_impl( + ptr, + rust_vec_len, + data_len, + ), + 33 => wire__crate__api__protocol__ProtocolProfile_oath_validate_impl( + ptr, + rust_vec_len, + data_len, + ), + 34 => wire__crate__api__protocol__ProtocolProfile_openpgp_activate_impl( + ptr, + rust_vec_len, + data_len, + ), + 35 => wire__crate__api__protocol__ProtocolProfile_openpgp_change_password_impl( + ptr, + rust_vec_len, + data_len, + ), + 36 => wire__crate__api__protocol__ProtocolProfile_openpgp_generate_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 37 => wire__crate__api__protocol__ProtocolProfile_openpgp_logout_impl( + ptr, + rust_vec_len, + data_len, + ), + 38 => wire__crate__api__protocol__ProtocolProfile_openpgp_pin_status_impl( + ptr, + rust_vec_len, + data_len, + ), + 39 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 40 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_data_impl( + ptr, + rust_vec_len, + data_len, + ), + 41 => wire__crate__api__protocol__ProtocolProfile_openpgp_reset_retries_impl( + ptr, + rust_vec_len, + data_len, + ), + 42 => wire__crate__api__protocol__ProtocolProfile_openpgp_terminate_impl( + ptr, + rust_vec_len, + data_len, + ), + 43 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_admin_impl( + ptr, + rust_vec_len, + data_len, + ), + 44 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_code_impl( + ptr, + rust_vec_len, + data_len, + ), + 45 => wire__crate__api__protocol__ProtocolProfile_openpgp_verify_impl( + ptr, + rust_vec_len, + data_len, + ), + 46 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 47 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_language_impl( + ptr, + rust_vec_len, + data_len, + ), + 48 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_login_impl( + ptr, + rust_vec_len, + data_len, + ), + 49 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_name_impl( + ptr, + rust_vec_len, + data_len, + ), + 50 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_reset_code_impl( + ptr, + rust_vec_len, + data_len, + ), + 51 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_sex_impl( + ptr, + rust_vec_len, + data_len, + ), + 52 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_signature_pin_policy_impl( + ptr, + rust_vec_len, + data_len, + ), + 53 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_cache_time_impl( + ptr, + rust_vec_len, + data_len, + ), + 54 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_policy_impl( + ptr, + rust_vec_len, + data_len, + ), + 55 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_url_impl( + ptr, + rust_vec_len, + data_len, + ), + 56 => wire__crate__api__protocol__ProtocolProfile_piv_algorithm_display_id_impl( + ptr, + rust_vec_len, + data_len, + ), + 57 => { + wire__crate__api__protocol__ProtocolProfile_piv_attest_impl(ptr, rust_vec_len, data_len) + } + 58 => wire__crate__api__protocol__ProtocolProfile_piv_authenticate_management_impl( + ptr, + rust_vec_len, + data_len, + ), + 59 => wire__crate__api__protocol__ProtocolProfile_piv_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 60 => wire__crate__api__protocol__ProtocolProfile_piv_credential_impl( + ptr, + rust_vec_len, + data_len, + ), + 61 => wire__crate__api__protocol__ProtocolProfile_piv_decapsulate_impl( + ptr, + rust_vec_len, + data_len, + ), + 62 => wire__crate__api__protocol__ProtocolProfile_piv_decrypt_impl( + ptr, + rust_vec_len, + data_len, + ), + 63 => wire__crate__api__protocol__ProtocolProfile_piv_delete_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 64 => wire__crate__api__protocol__ProtocolProfile_piv_delete_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 65 => { + wire__crate__api__protocol__ProtocolProfile_piv_derive_impl(ptr, rust_vec_len, data_len) + } + 66 => wire__crate__api__protocol__ProtocolProfile_piv_generate_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 67 => wire__crate__api__protocol__ProtocolProfile_piv_import_ec_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 68 => wire__crate__api__protocol__ProtocolProfile_piv_import_ed25519_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 69 => wire__crate__api__protocol__ProtocolProfile_piv_import_pq_seed_impl( + ptr, + rust_vec_len, + data_len, + ), + 70 => wire__crate__api__protocol__ProtocolProfile_piv_import_rsa_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 71 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_impl( + ptr, + rust_vec_len, + data_len, + ), + 72 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_directory_impl( + ptr, + rust_vec_len, + data_len, + ), + 73 => wire__crate__api__protocol__ProtocolProfile_piv_move_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 74 => wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( + ptr, + rust_vec_len, + data_len, + ), + 75 => wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( + ptr, + rust_vec_len, + data_len, + ), + 76 => wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( + ptr, + rust_vec_len, + data_len, + ), + 77 => wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( + ptr, + rust_vec_len, + data_len, + ), + 78 => wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( + ptr, + rust_vec_len, + data_len, + ), + 79 => { + wire__crate__api__protocol__ProtocolProfile_piv_sign_impl(ptr, rust_vec_len, data_len) + } + 80 => wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( + ptr, + rust_vec_len, + data_len, + ), + 81 => wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( + ptr, + rust_vec_len, + data_len, + ), + 82 => wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( + ptr, + rust_vec_len, + data_len, + ), + 83 => wire__crate__api__protocol__ProtocolProfile_serial_impl(ptr, rust_vec_len, data_len), + 84 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), + 85 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), + 86 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( + ptr, + rust_vec_len, + data_len, + ), + 87 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), + 88 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 89 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), + 90 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), + 91 => wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len), + 92 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( + ptr, + rust_vec_len, + data_len, + ), + 93 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), + 94 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), + 95 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), + 96 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( + ptr, + rust_vec_len, + data_len, + ), + 97 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), + 98 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( + ptr, + rust_vec_len, + data_len, + ), + 99 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( + ptr, + rust_vec_len, + data_len, + ), + 100 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), + 101 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), + 102 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), + 103 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), + 104 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), + 105 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), + _ => unreachable!(), } } -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } - } -} +// Section: rust2dart -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() } } +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode(deserializer)); - } else { - return None; - } +impl flutter_rust_bridge::IntoIntoDart> for ProtocolOperation { + fn into_into_dart(self) -> FrbWrapper { + self.into() } } -impl SseDecode for Option> { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(>::sse_decode(deserializer)); - } else { - return None; - } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() } } +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} -impl SseDecode for crate::api::piv_crypto::PivImportFileData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_privateKey = - >::sse_decode(deserializer); - let mut var_certificate = - >::sse_decode(deserializer); - return crate::api::piv_crypto::PivImportFileData { - private_key: var_privateKey, - certificate: var_certificate, - }; +impl flutter_rust_bridge::IntoIntoDart> for ProtocolProfile { + fn into_into_dart(self) -> FrbWrapper { + self.into() } } -impl SseDecode for crate::api::piv_crypto::PivPrivateKeyData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_algorithm = ::sse_decode(deserializer); - let mut var_importData = >::sse_decode(deserializer); - let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); - return crate::api::piv_crypto::PivPrivateKeyData { - algorithm: var_algorithm, - import_data: var_importData, - subject_public_key_info: var_subjectPublicKeyInfo, - }; +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminAction { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::VerifyPin => 0.into_dart(), + Self::ChangePin => 1.into_dart(), + Self::KeyboardInterface => 2.into_dart(), + Self::KeyboardReturn => 3.into_dart(), + Self::KeyboardKeymap => 4.into_dart(), + Self::ClearKeyboardKeymap => 5.into_dart(), + Self::LegacyPivExtensions => 6.into_dart(), + Self::LegacyTouch => 7.into_dart(), + Self::WriteSm2 => 8.into_dart(), + Self::Nfc => 9.into_dart(), + Self::ResetOpenPgp => 10.into_dart(), + Self::ResetPiv => 11.into_dart(), + Self::ResetOath => 12.into_dart(), + Self::ResetNdef => 13.into_dart(), + Self::ResetCtap => 14.into_dart(), + Self::ResetPass => 15.into_dart(), + Self::FactoryReset => 16.into_dart(), + _ => unreachable!(), + } } } - -impl SseDecode for crate::api::piv_crypto::PivPublicKeyData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); - let mut var_rawPublicKey = >::sse_decode(deserializer); - return crate::api::piv_crypto::PivPublicKeyData { - subject_public_key_info: var_subjectPublicKeyInfo, - raw_public_key: var_rawPublicKey, - }; - } +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminAction +{ } - -impl SseDecode for crate::api::protocol::PivReadOperation { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return match inner { - 0 => crate::api::protocol::PivReadOperation::Select, - 1 => crate::api::protocol::PivReadOperation::Version, - 2 => crate::api::protocol::PivReadOperation::AlgorithmConfiguration, - _ => unreachable!("Invalid variant for PivReadOperation: {}", inner), - }; +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminAction +{ + fn into_into_dart(self) -> crate::api::protocol::AdminAction { + self } } - -impl SseDecode for crate::api::protocol::ProtocolError { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_kind = ::sse_decode(deserializer); - let mut var_phase = ::sse_decode(deserializer); - let mut var_statusWord = >::sse_decode(deserializer); - let mut var_reference = >::sse_decode(deserializer); - let mut var_retriesRemaining = >::sse_decode(deserializer); - return crate::api::protocol::ProtocolError { - kind: var_kind, - phase: var_phase, - status_word: var_statusWord, - reference: var_reference, - retries_remaining: var_retriesRemaining, - }; +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminConfigurationPatch { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.led_on.into_into_dart().into_dart(), + self.ndef_read_only.into_into_dart().into_dart(), + self.ndef_enabled.into_into_dart().into_dart(), + self.webusb_landing.into_into_dart().into_dart(), + self.feature_mask.into_into_dart().into_dart(), + self.feature_values.into_into_dart().into_dart(), + ] + .into_dart() } } - -impl SseDecode for crate::api::protocol::ProtocolStep { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_command = >>::sse_decode(deserializer); - let mut var_data = >>::sse_decode(deserializer); - let mut var_error = >::sse_decode(deserializer); - return crate::api::protocol::ProtocolStep { - command: var_command, - data: var_data, - error: var_error, - }; - } +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminConfigurationPatch +{ } - -impl SseDecode for crate::api::piv_crypto::SelfSignedCertificateParams { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_commonName = ::sse_decode(deserializer); - let mut var_organization = >::sse_decode(deserializer); - let mut var_organizationalUnit = >::sse_decode(deserializer); - let mut var_country = >::sse_decode(deserializer); - let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); - let mut var_serialNumber = >::sse_decode(deserializer); - let mut var_notBefore = ::sse_decode(deserializer); - let mut var_notAfter = ::sse_decode(deserializer); - let mut var_subjectAlternativeNames = >::sse_decode(deserializer); - let mut var_keyUsage = ::sse_decode(deserializer); - let mut var_keyUsageCritical = ::sse_decode(deserializer); - let mut var_extendedKeyUsage = >::sse_decode(deserializer); - let mut var_includeBasicConstraints = ::sse_decode(deserializer); - return crate::api::piv_crypto::SelfSignedCertificateParams { - common_name: var_commonName, - organization: var_organization, - organizational_unit: var_organizationalUnit, - country: var_country, - subject_public_key_info: var_subjectPublicKeyInfo, - serial_number: var_serialNumber, - not_before: var_notBefore, - not_after: var_notAfter, - subject_alternative_names: var_subjectAlternativeNames, - key_usage: var_keyUsage, - key_usage_critical: var_keyUsageCritical, - extended_key_usage: var_extendedKeyUsage, - include_basic_constraints: var_includeBasicConstraints, - }; +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminConfigurationPatch +{ + fn into_into_dart(self) -> crate::api::protocol::AdminConfigurationPatch { + self } } - -impl SseDecode for u16 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u16::().unwrap() +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminProgress { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.confirmed_writes.into_into_dart().into_dart(), + self.reprobe_required.into_into_dart().into_dart(), + ] + .into_dart() } } - -impl SseDecode for u32 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u32::().unwrap() - } +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminProgress +{ } - -impl SseDecode for u8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u8().unwrap() +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminProgress +{ + fn into_into_dart(self) -> crate::api::protocol::AdminProgress { + self } } - -impl SseDecode for () { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} -} - -impl SseDecode for usize { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_u64::().unwrap() as _ +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminReadOperation { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Firmware => 0.into_dart(), + Self::Model => 1.into_dart(), + Self::Serial => 2.into_dart(), + Self::ChipId => 3.into_dart(), + Self::CoreCommit => 4.into_dart(), + Self::Configuration => 5.into_dart(), + Self::FlashUsage => 6.into_dart(), + Self::AppletUsage => 7.into_dart(), + Self::KeyboardLayout => 8.into_dart(), + Self::KeyboardKeymap => 9.into_dart(), + Self::NfcStatus => 10.into_dart(), + Self::Sm2Configuration => 11.into_dart(), + _ => unreachable!(), + } } } - -impl SseDecode for crate::api::crypto::X509CertData { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_bytes = >::sse_decode(deserializer); - let mut var_subject = ::sse_decode(deserializer); - let mut var_issuer = ::sse_decode(deserializer); - let mut var_notBefore = ::sse_decode(deserializer); - let mut var_notAfter = ::sse_decode(deserializer); - let mut var_serialNumber = ::sse_decode(deserializer); - let mut var_signatureAlgorithm = ::sse_decode(deserializer); - let mut var_signatureValue = >::sse_decode(deserializer); - let mut var_publicKeyAlgorithm = ::sse_decode(deserializer); - let mut var_publicKeySize = ::sse_decode(deserializer); - let mut var_subjectPublicKeyInfo = >::sse_decode(deserializer); - let mut var_rawPublicKey = >::sse_decode(deserializer); - return crate::api::crypto::X509CertData { - bytes: var_bytes, - subject: var_subject, - issuer: var_issuer, - not_before: var_notBefore, - not_after: var_notAfter, - serial_number: var_serialNumber, - signature_algorithm: var_signatureAlgorithm, - signature_value: var_signatureValue, - public_key_algorithm: var_publicKeyAlgorithm, - public_key_size: var_publicKeySize, - subject_public_key_info: var_subjectPublicKeyInfo, - raw_public_key: var_rawPublicKey, - }; +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminReadOperation +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminReadOperation +{ + fn into_into_dart(self) -> crate::api::protocol::AdminReadOperation { + self } } - -fn pde_ffi_dispatcher_primary_impl( - func_id: i32, - port: flutter_rust_bridge::for_generated::MessagePort, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - _ => unreachable!(), +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminResult { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.kind.into_into_dart().into_dart(), + self.data.into_into_dart().into_dart(), + self.progress.into_into_dart().into_dart(), + ] + .into_dart() } } - -fn pde_ffi_dispatcher_sync_impl( - func_id: i32, - ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len: i32, - data_len: i32, -) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { - // Codec=Pde (Serialization + dispatch), see doc to use other codecs - match func_id { - 1 => { - wire__crate__api__protocol__ProtocolOperation_advance_impl(ptr, rust_vec_len, data_len) +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminResult +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminResult +{ + fn into_into_dart(self) -> crate::api::protocol::AdminResult { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::AdminValueKind { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::None => 0.into_dart(), + Self::Bytes => 1.into_dart(), + Self::Configuration => 2.into_dart(), + Self::LegacyConfiguration => 3.into_dart(), + Self::FlashUsage => 4.into_dart(), + Self::AppletUsage => 5.into_dart(), + Self::NfcStatus => 6.into_dart(), + Self::Sm2Configuration => 7.into_dart(), + Self::LegacySm2Configuration => 8.into_dart(), + Self::PinStatus => 9.into_dart(), + Self::KeyboardLayout => 10.into_dart(), + Self::KeyboardKeymap => 11.into_dart(), + Self::PassSlots => 12.into_dart(), + _ => unreachable!(), } - 2 => wire__crate__api__protocol__ProtocolOperation_close_impl(ptr, rust_vec_len, data_len), - 3 => { - wire__crate__api__protocol__ProtocolOperation_piv_read_impl(ptr, rust_vec_len, data_len) + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::AdminValueKind +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::AdminValueKind +{ + fn into_into_dart(self) -> crate::api::protocol::AdminValueKind { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::protocol::BootstrapIdentityStep { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::SelectAdmin => 0.into_dart(), + Self::Serial => 1.into_dart(), + _ => unreachable!(), } - 4 => wire__crate__api__protocol__ProtocolOperation_start_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), - 6 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), - 7 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( - ptr, - rust_vec_len, - data_len, - ), - 8 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), - 9 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( - ptr, - rust_vec_len, - data_len, - ), - 10 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), - 11 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), - 12 => wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len), - 13 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( - ptr, - rust_vec_len, - data_len, - ), - 14 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), - 16 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), - 17 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( - ptr, - rust_vec_len, - data_len, - ), - 18 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), - 19 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( - ptr, - rust_vec_len, - data_len, - ), - 20 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( - ptr, - rust_vec_len, - data_len, - ), - 21 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), - 22 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), - 23 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), - 24 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), - 25 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), - _ => unreachable!(), } } - -// Section: rust2dart - +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::BootstrapIdentityStep +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::BootstrapIdentityStep +{ + fn into_into_dart(self) -> crate::api::protocol::BootstrapIdentityStep { + self + } +} // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for FrbWrapper { +impl flutter_rust_bridge::IntoDart for crate::api::protocol::PivCredentialOperation { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) - .into_dart() + match self { + Self::VerifyPin => 0.into_dart(), + Self::ChangePin => 1.into_dart(), + Self::ChangePuk => 2.into_dart(), + Self::UnblockPin => 3.into_dart(), + Self::Logout => 4.into_dart(), + _ => unreachable!(), + } } } -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} - -impl flutter_rust_bridge::IntoIntoDart> for ProtocolOperation { - fn into_into_dart(self) -> FrbWrapper { - self.into() +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::protocol::PivCredentialOperation +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::protocol::PivCredentialOperation +{ + fn into_into_dart(self) -> crate::api::protocol::PivCredentialOperation { + self } } - // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::piv_crypto::PivImportFileData { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -1477,6 +6360,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::protocol::PivReadOperation { Self::Select => 0.into_dart(), Self::Version => 1.into_dart(), Self::AlgorithmConfiguration => 2.into_dart(), + Self::PinStatus => 3.into_dart(), _ => unreachable!(), } } @@ -1523,6 +6407,8 @@ impl flutter_rust_bridge::IntoDart for crate::api::protocol::ProtocolStep { self.command.into_into_dart().into_dart(), self.data.into_into_dart().into_dart(), self.error.into_into_dart().into_dart(), + self.profile.into_into_dart().into_dart(), + self.admin.into_into_dart().into_dart(), ] .into_dart() } @@ -1609,6 +6495,13 @@ impl SseEncode for ProtocolOperation { } } +impl SseEncode for ProtocolProfile { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for RustOpaqueMoi> { @@ -1620,6 +6513,17 @@ impl SseEncode } } +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1627,6 +6531,119 @@ impl SseEncode for String { } } +impl SseEncode for crate::api::protocol::AdminAction { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::AdminAction::VerifyPin => 0, + crate::api::protocol::AdminAction::ChangePin => 1, + crate::api::protocol::AdminAction::KeyboardInterface => 2, + crate::api::protocol::AdminAction::KeyboardReturn => 3, + crate::api::protocol::AdminAction::KeyboardKeymap => 4, + crate::api::protocol::AdminAction::ClearKeyboardKeymap => 5, + crate::api::protocol::AdminAction::LegacyPivExtensions => 6, + crate::api::protocol::AdminAction::LegacyTouch => 7, + crate::api::protocol::AdminAction::WriteSm2 => 8, + crate::api::protocol::AdminAction::Nfc => 9, + crate::api::protocol::AdminAction::ResetOpenPgp => 10, + crate::api::protocol::AdminAction::ResetPiv => 11, + crate::api::protocol::AdminAction::ResetOath => 12, + crate::api::protocol::AdminAction::ResetNdef => 13, + crate::api::protocol::AdminAction::ResetCtap => 14, + crate::api::protocol::AdminAction::ResetPass => 15, + crate::api::protocol::AdminAction::FactoryReset => 16, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::api::protocol::AdminConfigurationPatch { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.led_on, serializer); + >::sse_encode(self.ndef_read_only, serializer); + >::sse_encode(self.ndef_enabled, serializer); + >::sse_encode(self.webusb_landing, serializer); + ::sse_encode(self.feature_mask, serializer); + ::sse_encode(self.feature_values, serializer); + } +} + +impl SseEncode for crate::api::protocol::AdminProgress { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.confirmed_writes, serializer); + ::sse_encode(self.reprobe_required, serializer); + } +} + +impl SseEncode for crate::api::protocol::AdminReadOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::AdminReadOperation::Firmware => 0, + crate::api::protocol::AdminReadOperation::Model => 1, + crate::api::protocol::AdminReadOperation::Serial => 2, + crate::api::protocol::AdminReadOperation::ChipId => 3, + crate::api::protocol::AdminReadOperation::CoreCommit => 4, + crate::api::protocol::AdminReadOperation::Configuration => 5, + crate::api::protocol::AdminReadOperation::FlashUsage => 6, + crate::api::protocol::AdminReadOperation::AppletUsage => 7, + crate::api::protocol::AdminReadOperation::KeyboardLayout => 8, + crate::api::protocol::AdminReadOperation::KeyboardKeymap => 9, + crate::api::protocol::AdminReadOperation::NfcStatus => 10, + crate::api::protocol::AdminReadOperation::Sm2Configuration => 11, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::api::protocol::AdminResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.kind, serializer); + >::sse_encode(self.data, serializer); + ::sse_encode(self.progress, serializer); + } +} + +impl SseEncode for crate::api::protocol::AdminValueKind { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::AdminValueKind::None => 0, + crate::api::protocol::AdminValueKind::Bytes => 1, + crate::api::protocol::AdminValueKind::Configuration => 2, + crate::api::protocol::AdminValueKind::LegacyConfiguration => 3, + crate::api::protocol::AdminValueKind::FlashUsage => 4, + crate::api::protocol::AdminValueKind::AppletUsage => 5, + crate::api::protocol::AdminValueKind::NfcStatus => 6, + crate::api::protocol::AdminValueKind::Sm2Configuration => 7, + crate::api::protocol::AdminValueKind::LegacySm2Configuration => 8, + crate::api::protocol::AdminValueKind::PinStatus => 9, + crate::api::protocol::AdminValueKind::KeyboardLayout => 10, + crate::api::protocol::AdminValueKind::KeyboardKeymap => 11, + crate::api::protocol::AdminValueKind::PassSlots => 12, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1634,6 +6651,22 @@ impl SseEncode for bool { } } +impl SseEncode for crate::api::protocol::BootstrapIdentityStep { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::BootstrapIdentityStep::SelectAdmin => 0, + crate::api::protocol::BootstrapIdentityStep::Serial => 1, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1678,6 +6711,46 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1738,6 +6811,25 @@ impl SseEncode for Option> { } } +impl SseEncode for crate::api::protocol::PivCredentialOperation { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::protocol::PivCredentialOperation::VerifyPin => 0, + crate::api::protocol::PivCredentialOperation::ChangePin => 1, + crate::api::protocol::PivCredentialOperation::ChangePuk => 2, + crate::api::protocol::PivCredentialOperation::UnblockPin => 3, + crate::api::protocol::PivCredentialOperation::Logout => 4, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for crate::api::piv_crypto::PivImportFileData { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1774,6 +6866,7 @@ impl SseEncode for crate::api::protocol::PivReadOperation { crate::api::protocol::PivReadOperation::Select => 0, crate::api::protocol::PivReadOperation::Version => 1, crate::api::protocol::PivReadOperation::AlgorithmConfiguration => 2, + crate::api::protocol::PivReadOperation::PinStatus => 3, _ => { unimplemented!(""); } @@ -1800,6 +6893,8 @@ impl SseEncode for crate::api::protocol::ProtocolStep { >>::sse_encode(self.command, serializer); >>::sse_encode(self.data, serializer); >::sse_encode(self.error, serializer); + >::sse_encode(self.profile, serializer); + >::sse_encode(self.admin, serializer); } } @@ -1908,6 +7003,20 @@ mod io { ) { MoiArc::>::decrement_strong_count(ptr as _); } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } } #[cfg(not(target_family = "wasm"))] pub use io::*; @@ -1947,6 +7056,20 @@ mod web { ) { MoiArc::>::decrement_strong_count(ptr as _); } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } } #[cfg(target_family = "wasm")] pub use web::*; diff --git a/test/controller/applets/piv/piv_certificate_loading_test.dart b/test/controller/applets/piv/piv_certificate_loading_test.dart index 3608754..6f49717 100644 --- a/test/controller/applets/piv/piv_certificate_loading_test.dart +++ b/test/controller/applets/piv/piv_certificate_loading_test.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:canokey_console/src/rust/api/crypto.dart'; import 'package:canokey_console/src/rust/frb_generated.dart'; @@ -45,16 +46,18 @@ void main() { final api = PivCertificateApi(); RustLib.initMock(api: api); addTearDown(RustLib.dispose); - final transport = CertificateTransport(['9000', '530570033001009000']); + final transport = CertificateTransport(['9000']); final controller = PivController( client: PivCardClient( transport: transport, - readExecutor: (kind) async { - expect(kind, PivReadOperation.select); + certificateExecutor: (objectId) async { + expect(objectId, 0x05); + return Uint8List.fromList([0x30, 1, 0]); + }, + prepareExecutor: () async { SmartCard.assertOK( await transport.transceive('00A4040005A00000030800'), ); - return Uint8List(0); }, ), ); @@ -69,35 +72,35 @@ void main() { ); expect(controller.publicKeyDerForSlot(0x9d), isNull); expect(controller.slots, isEmpty); - expect(transport.commands, [ - '00A4040005A00000030800', - '00CB3FFF055C035FC10500', - ]); + expect(transport.commands, ['00A4040005A00000030800']); }, ); test( 'unreadable certificate stays occupied and does not block the next slot', () async { - for (final response in [ - '5301709000', // Truncated certificate object. - '530a7003300100710101fe009000', // Compressed certificate. + for (final kind in [ + 'InvalidResponse', + 'UnsupportedProtocolVersion', + 'LimitExceeded', ]) { - final transport = CertificateTransport([ - '9000', - response, - '9000', - '6A82', - ]); + final transport = CertificateTransport(['9000', '9000']); final controller = PivController( client: PivCardClient( transport: transport, - readExecutor: (kind) async { - expect(kind, PivReadOperation.select); + certificateExecutor: (objectId) async { + throw ProtocolException( + ProtocolError( + kind: objectId == 0x05 ? kind : 'NotFound', + phase: objectId == 0x05 ? 'Parsing' : 'Command', + statusWord: objectId == 0x05 ? null : 0x6a82, + ), + ); + }, + prepareExecutor: () async { SmartCard.assertOK( await transport.transceive('00A4040005A00000030800'), ); - return Uint8List(0); }, ), ); @@ -111,9 +114,7 @@ void main() { expect(transport.responses, isEmpty); expect(transport.commands, [ '00A4040005A00000030800', - '00CB3FFF055C035FC10500', '00A4040005A00000030800', - '00CB3FFF055C035FC10b00', ]); } }, @@ -123,16 +124,18 @@ void main() { () async { RustLib.initMock(api: InvalidCertificateApi()); addTearDown(RustLib.dispose); - final transport = CertificateTransport(['9000', '530570033001009000']); + final transport = CertificateTransport(['9000']); final controller = PivController( client: PivCardClient( transport: transport, - readExecutor: (kind) async { - expect(kind, PivReadOperation.select); + certificateExecutor: (objectId) async { + expect(objectId, 0x05); + return Uint8List.fromList([0x30, 1, 0]); + }, + prepareExecutor: () async { SmartCard.assertOK( await transport.transceive('00A4040005A00000030800'), ); - return Uint8List(0); }, ), ); diff --git a/test/controller/applets/settings/settings_configuration_test.dart b/test/controller/applets/settings/settings_configuration_test.dart index 77d2d2c..32c7280 100644 --- a/test/controller/applets/settings/settings_configuration_test.dart +++ b/test/controller/applets/settings/settings_configuration_test.dart @@ -43,6 +43,8 @@ class _Controller extends SettingsController { _Controller(AdminCardClient client) : super(client: client); @override Future authenticate(String sn) async => true; + @override + String get adminPinForCurrentLease => '123456'; } class _Card extends AdminCardClient { @@ -50,13 +52,16 @@ class _Card extends AdminCardClient { final String firmware; final List config; @override + Future prepare() async {} + @override Future readFirmwareVersion() async => firmware; @override Future readModel() async => 'CanoKey'; @override Future readChipId() async => '0102'; @override - Future readConfig() async => Uint8List.fromList(config); + Future readConfig({String? pin}) async => + Uint8List.fromList(config); @override - Future readNfcEnabled() async => true; + Future readNfcEnabled({String? pin}) async => true; } diff --git a/test/controller/applets/webauthn/ctap_transmitter_test.dart b/test/controller/applets/webauthn/ctap_transmitter_test.dart index a947c89..5da26ba 100644 --- a/test/controller/applets/webauthn/ctap_transmitter_test.dart +++ b/test/controller/applets/webauthn/ctap_transmitter_test.dart @@ -1,28 +1,118 @@ +@Tags(['native']) +library; + import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/helper/utils/ctap_transmitter.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('frames CTAP commands and follows GET RESPONSE', () async { - final transport = _QueueApduTransport(['006102', 'A19000']); + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); + + test('selects once, frames CTAP commands and follows GET RESPONSE', () async { + final transport = _QueueApduTransport(['9000', '006102', 'A19000']); final transmitter = CtapTransmitter(transport: transport); final response = await transmitter.transceive([0x04]); expect(response.status, 0); expect(response.data, [0xA1]); - expect(transport.commands, ['801000000104', '80C0000002']); + expect(transport.commands, [ + '00A4040008A0000006472F0001', + '801000000104', + '80C0000002', + ]); }); - test('uses extended APDU length for large CTAP commands', () async { - final transport = _QueueApduTransport(['009000']); + test('reuses the selection for subsequent messages', () async { + final transport = _QueueApduTransport(['9000', '009000', '2E9000']); final transmitter = CtapTransmitter(transport: transport); - final response = await transmitter.transceive(List.filled(256, 0xAA)); + await transmitter.transceive([0x04]); + final response = await transmitter.transceive([0x0A]); - expect(response.status, 0); + // A non-success CTAP status byte is data, not an error. + expect(response.status, 0x2E); expect(response.data, isEmpty); - expect(transport.commands.single, startsWith('80100000000100')); + expect(transport.commands, [ + '00A4040008A0000006472F0001', + '801000000104', + '80100000010A', + ]); + }); + + test('explicit reselection selects again', () async { + final transport = _QueueApduTransport(['9000', '009000', '9000']); + final transmitter = CtapTransmitter(transport: transport); + + await transmitter.transceive([0x04]); + await transmitter.selectApplication(); + + expect(transport.commands, [ + '00A4040008A0000006472F0001', + '801000000104', + '00A4040008A0000006472F0001', + ]); + }); + + test('a new transmitter without lease evidence selects again', () async { + // Without a lease there is no shared selection evidence, so a new + // transmitter selects the applet before its first message. + final transport = _QueueApduTransport(['9000', '009000']); + final response = await CtapTransmitter( + transport: transport, + ).transceive([0x04]); + expect(response.status, 0); + expect(transport.commands, [ + '00A4040008A0000006472F0001', + '801000000104', + ]); + }); + + test('transmitters sharing a lease select the applet once', () async { + final sessions = CardSessions(); + final transport = _QueueApduTransport(['9000', '009000', '009000']); + await sessions.run((session) async { + session.bind(transport.transceive); + final first = CtapTransmitter( + transport: transport, + lease: session.lease, + ); + await first.transceive([0x04]); + // The selection belongs to the lease, so a second transmitter instance + // reuses it instead of selecting again. + final second = CtapTransmitter( + transport: transport, + lease: session.lease, + ); + final response = await second.transceive([0x0A]); + expect(response.status, 0); + }); + expect(transport.commands, [ + '00A4040008A0000006472F0001', + '801000000104', + '80100000010A', + ]); + }); + + test('an absent FIDO2 applet surfaces as UnsupportedDevice', () async { + final transport = _QueueApduTransport(['6A82']); + final transmitter = CtapTransmitter(transport: transport); + + await expectLater( + transmitter.transceive([0x04]), + throwsA( + isA().having( + (error) => error.details.kind, + 'kind', + 'UnsupportedDevice', + ), + ), + ); + expect(transport.commands, ['00A4040008A0000006472F0001']); }); } diff --git a/test/helper/storage/credential_cache_test.dart b/test/helper/storage/credential_cache_test.dart new file mode 100644 index 0000000..763796f --- /dev/null +++ b/test/helper/storage/credential_cache_test.dart @@ -0,0 +1,58 @@ +import 'package:canokey_console/helper/storage/local_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await LocalStorage.init(); + }); + + test( + 'PIN invalidation reaches all page-local copies before the next attempt', + () async { + final settings = CredentialCache('ADMIN')..['A'] = 'old-pin'; + final pass = CredentialCache('ADMIN')..['A'] = 'old-pin'; + await LocalStorage.setPinCache('A', 'ADMIN', 'old-pin'); + await LocalStorage.setPinCache('A', 'ADMIN', null); + expect(settings.containsKey('A'), isFalse); + expect(pass['A'], isNull); + expect(LocalStorage.getPinCache('A', 'ADMIN'), isNull); + await LocalStorage.setPinCache('A', 'ADMIN', 'new-pin'); + expect(pass['A'], isNull); // Saving a value is not new verification. + settings['A'] = 'new-pin'; + expect(settings['A'], 'new-pin'); + }, + ); + + test( + 'device reset invalidates saved and unsaved credentials for only that device', + () async { + final admin = CredentialCache('ADMIN') + ..['A'] = 'admin-a' + ..['B'] = 'admin-b'; + final oath = CredentialCache('OATH')..['A'] = 'unsaved-oath'; + final webauthn = CredentialCache('webauthn')..['A'] = 'fido-a'; + await LocalStorage.setPinCache('A', 'webauthn', 'fido-a'); + await LocalStorage.setPinCache('B', 'ADMIN', 'admin-b'); + await LocalStorage.setStartPage('settings'); + await LocalStorage.clearPinCacheForDevice('A'); + expect(admin['A'], isNull); + expect(oath['A'], isNull); + expect(webauthn['A'], isNull); + expect(LocalStorage.getPinCache('A', 'webauthn'), isNull); + expect(admin['B'], 'admin-b'); + expect(LocalStorage.getPinCache('B', 'ADMIN'), 'admin-b'); + expect(LocalStorage.getStartPage(), 'settings'); + }, + ); + + test( + 'global cache clearing also invalidates credentials never saved to disk', + () async { + final cache = CredentialCache('ADMIN')..['unsaved'] = 'pin'; + await LocalStorage.clearPinCache(); + expect(cache['unsaved'], isNull); + }, + ); +} diff --git a/test/helper/utils/admin_card_test.dart b/test/helper/utils/admin_card_test.dart index 531bbad..738c17a 100644 --- a/test/helper/utils/admin_card_test.dart +++ b/test/helper/utils/admin_card_test.dart @@ -1,213 +1,641 @@ +@Tags(['native']) +library; + +import 'dart:async'; +import 'dart:convert'; import 'package:canokey_console/helper/utils/admin_card.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/piv_card.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; +import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('reads current SM2 defaults and writes big-endian signed IDs', () async { - final transport = _QueueApduTransport([ - '00000009ffffffca9000', - '00000009ffffffca9000', - '9000', - ]); - final client = AdminCardClient(transport: transport); - final config = await client.readSm2Config(); - expect(config.enabled, isTrue); - expect(config.canChangeEnabled, isFalse); - expect(config.curveId, 9); - expect(config.algoId, -54); - await client.writeSm2Config( - enabled: false, - curveId: -2147483648, - algoId: 2147483647, - ); - expect(transport.commands, [ - '0011000000', - '0011000000', - '0012000008800000007fffffff', - ]); - }); + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); test( - 'preserves the legacy enabled byte when writing SM2 configuration', + 'minimal discovery owns identity evidence without selecting PIV', () async { - final transport = _QueueApduTransport(['0100000009ffffffd09000', '9000']); - final client = AdminCardClient(transport: transport); - await client.writeSm2Config(enabled: false, curveId: 9, algoId: -48); - expect(transport.commands, [ - '0011000000', - '00120000090000000009ffffffd0', + final transport = _Transport([]); + await _prepared(transport, (client) async { + expect(await client.readFirmwareVersion(), '3.1.0'); + expect(await client.readModel(), 'CanoKey'); + expect(await client.readSerial(), '01020304'); + expect(transport.commands, isEmpty); + }); + }, + ); + + test( + 'configuration, flash and applet usage use upstream validated results', + () async { + final usage = List.filled(48, 0) + ..[0] = 7 + ..[1] = 0x81 + ..[5] = 42; + final transport = _Transport([ + '9000', + '01000001013F9000', + '9000', + '02089000', + '9000', + '${hex.encode(usage)}9000', + '9000', + 'AABB9000', ]); + await _prepared(transport, (client) async { + expect(await client.readConfig(), [1, 0, 0, 1, 1, 63]); + final flash = await client.readStorageUsage(); + expect((flash.usedKiB, flash.totalKiB), (2, 8)); + expect(await client.readAppletStorageUsage(), usage); + expect(await client.readChipId(), 'AABB'); + expect(transport.commands, [ + _select, + '0042000000', + _select, + '0041000000', + _select, + '0041010000', + _select, + '0032010000', + ]); + }); }, ); - test('rejects reserved current SM2 IDs before sending a write', () async { - final transport = _QueueApduTransport(['00000009ffffffca9000']); - await expectLater( - AdminCardClient( - transport: transport, - ).writeSm2Config(enabled: true, curveId: 1, algoId: -54), - throwsArgumentError, - ); - expect(transport.commands, ['0011000000']); - }); + test( + 'unverified protected reads still require an explicit per-request PIN', + () async { + final transport = _Transport(['9000', '9000', '0100010101019000']); + await _prepared(transport, (client) async { + await expectLater(client.readConfig(), _kind('SecurityStatusNotSatisfied')); + expect(transport.commands, isEmpty); + expect(await client.readConfig(pin: '123456'), [1, 0, 1, 1, 1, 1]); + expect(transport.commands, ['${_select}00', '${_verify}00', '0042000000']); + }, firmware: '2.0.0'); + }, + ); - test('rejects failed and malformed SM2 reads', () async { - await expectLater( - AdminCardClient(transport: _QueueApduTransport(['6982'])).readSm2Config(), - throwsA( - isA().having( - (error) => error.toString(), - 'message', - 'Exception: SW is not ok', - ), - ), - ); - await expectLater( - AdminCardClient( - transport: _QueueApduTransport(['00009000']), - ).readSm2Config(), - throwsFormatException, - ); - }); + test( + 'a verified session reuses its selection until a mutation ends the evidence', + () async { + final transport = _Transport([ + '9000', + '9000', + '0100010101019000', + '0100010101019000', + '9000', + '9000', + '9000', + '0100010101019000', + ]); + await _prepared(transport, (client) async { + expect(await client.verifyPin('123456'), isTrue); + // No SELECT/VERIFY: the recorded session authorizes Existing reads, + // and a supplied PIN stays unused while the evidence is valid. + expect(await client.readConfig(), [1, 0, 1, 1, 1, 1]); + expect(await client.readConfig(pin: '000000'), [1, 0, 1, 1, 1, 1]); + // A confirmed mutation ends the evidence without touching the profile. + await client.changePin('654321', currentPin: '123456'); + expect(client.lastProgress!.confirmedWrites, 1); + expect(client.lastProgress!.reprobeRequired, isFalse); + // Later protected requests fall back to an explicit per-request PIN. + expect(await client.readConfig(pin: '654321'), [1, 0, 1, 1, 1, 1]); + expect(transport.commands, [ + '${_select}00', + '${_verify}00', + '0042000000', + '0042000000', + '002100000636353433323100', + '${_select}00', + '002000000636353433323100', + '0042000000', + ]); + }, firmware: '2.0.0'); + }, + ); - test('reads and updates admin data through the injected transport', () async { - final transport = _QueueApduTransport([ - '9000', - '9000', - '312E339000', - '434B9000', - '010203049000', - 'AABB9000', - '0100019000', - '019000', - '01029000', - '9000', - ]); - final client = AdminCardClient(transport: transport); - - await client.select(); - expect(await client.verifyPin('123456'), isTrue); - expect(await client.readFirmwareVersion(), '1.3'); - expect(await client.readModel(), 'CK'); - expect(await client.readSerial(), '01020304'); - expect(await client.readChipId(), 'AABB'); - expect(await client.readConfig(), [1, 0, 1]); - expect(await client.readNfcEnabled(), isTrue); - final storage = await client.readStorageUsage(); - expect(storage.usedKiB, 1); - expect(storage.totalKiB, 2); - await client.writeConfigByte(1, 0); + test( + 'another applet selection on the lease ends the Admin session evidence', + () async { + final transport = _Transport([ + ..._probe('3.1.0'), + '9000', + '9000', + ..._probe('3.1.0'), + '9000', + '0600009000', + '01E00516E1531554E2E39000', + '9000', + '9000', + '9000', + ]); + await CardSessions().run((session) async { + session.bind(transport.transceive); + final admin = AdminCardClient( + transport: transport, + lease: session.lease, + ); + final piv = PivCardClient(transport: transport, lease: session.lease); + await admin.prepare(); + transport.commands.clear(); + expect(await admin.verifyPin('123456'), isTrue); + final verified = transport.commands.length; + await piv.prepare(); + // PIV selection replaced the Admin selection; the next protected + // request must SELECT and explicitly verify again. + await admin.setNfcEnabled(false, pin: '123456'); + expect(transport.commands.sublist(0, verified), [_select, _verify]); + expect(transport.commands.sublist(transport.commands.length - 3), [ + _select, + _verify, + '00140100', + ]); + }); + }, + ); - expect(transport.commands, [ - '00A4040005F000000000', - '0020000006313233343536', - '0031000000', - '0031010000', - '0032000000', - '0032010000', - '0042000000', - '0014000000', - '0041000002', - '00400100', - ]); - }); + test( + 'a failed Existing request ends the session evidence conservatively', + () async { + final transport = _Transport([ + '9000', + '9000', + '6982', + '9000', + '9000', + '9000', + ]); + await _prepared(transport, (client) async { + expect(await client.verifyPin('123456'), isTrue); + await expectLater( + client.readConfig(), + _kind('SecurityStatusNotSatisfied'), + ); + expect(transport.commands, [_select, _verify, '0042000000']); + // The card rejected the session, so the evidence is dropped and the + // next protected request SELECTs and verifies its explicit PIN. + await client.setNfcEnabled(false, pin: '123456'); + expect(transport.commands, [ + _select, + _verify, + '0042000000', + _select, + _verify, + '00140100', + ]); + }); + }, + ); test( - 'runs optional admin operations and handles unavailable core commit', + 'configuration layout follows firmware instead of response length', () async { - final transport = _QueueApduTransport(List.filled(5, '9000')); - final client = AdminCardClient(transport: transport); + for (final entry in [ + ('1.3', '010101010001099000'), + ('1.5.2', '01000101019000'), + ('1.6.2', '0100010101019000'), + ('3.0.3', '01FF000101FF9000'), + ]) { + await _prepared(_Transport(['9000', '9000', entry.$2]), (client) async { + expect( + await client.readConfig(pin: '123456'), + hex.decode(entry.$2.substring(0, entry.$2.length - 4)), + ); + }, firmware: entry.$1); + } + for (final response in ['0100019000', '02000001013F9000']) { + await _prepared(_Transport(['9000', response]), (client) async { + await expectLater(client.readConfig(), _kind('InvalidResponse')); + }); + } + }, + ); + + test( + 'PIN input is UTF8, owned, bounded, and failed VERIFY never reaches a write', + () async { + final transport = _Transport(['9000', '63C2', '9000', '9000', '9000']); + await _prepared(transport, (client) async { + await expectLater( + client.changePin('654321', currentPin: '123456'), + _kind('AuthenticationFailed'), + ); + expect(client.lastResponse, '63C2'); + expect(client.lastProgress!.confirmedWrites, 0); + expect(transport.commands, [_select, _verify]); + await client.changePin('CanoKey密码', currentPin: '123456'); + expect(transport.commands.last, '002100000D43616E6F4B6579E5AF86E7A081'); + expect(client.lastProgress!.confirmedWrites, 1); + expect(client.lastProgress!.reprobeRequired, isFalse); + final count = transport.commands.length; + for (final pin in ['', '12345', List.filled(22, '密').join()]) { + await expectLater(client.verifyPin(pin), _kind('InvalidPin')); + } + expect(transport.commands, hasLength(count)); + }); + }, + ); - await client.setNfcEnabled(true); - await client.setNfcEnabled(false); - await client.setNdefReadOnly(true); - await client.setNdefReadOnly(false); - await client.resetNdef(); - - expect(transport.commands, [ - '00140101', - '00140100', - '00080100', - '00080000', - '00070000', + test( + 'configuration patches preserve unselected fields and require fresh discovery after writes', + () async { + final transport = _Transport([ + '9000', + '9000', + '01000001013E9000', + '9000', + '9000', ]); - expect( - await AdminCardClient( - transport: _QueueApduTransport(['6162639000']), - ).readCoreCommit(), - 'abc', - ); - expect( - await AdminCardClient( - transport: _QueueApduTransport(['9000']), - ).readCoreCommit(), - isNull, + await _prepared(transport, (client) async { + await client.configure( + pin: '123456', + ledOn: false, + featureMask: 1, + featureValues: 1, + ); + expect(transport.commands, [ + _select, + _verify, + '0042000000', + '00400100', + '0040063F', + ]); + expect(client.lastProgress!.confirmedWrites, 2); + expect(client.lastProgress!.reprobeRequired, isTrue); + await expectLater(client.readConfig(), throwsStateError); + await expectLater(client.readFirmwareVersion(), throwsStateError); + }); + }, + ); + + test( + 'partial configuration failure retains progress and never resumes or rolls back', + () async { + final transport = _Transport([ + '9000', + '9000', + '01000001013F9000', + '9000', + '6F00', + ]); + await _prepared(transport, (client) async { + await expectLater( + client.configure(pin: '123456', ledOn: false, ndefEnabled: false), + _kind('UnexpectedStatusWord'), + ); + expect(client.lastProgress!.confirmedWrites, 1); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(transport.commands, [ + _select, + _verify, + '0042000000', + '00400100', + '00400400', + ]); + await expectLater(client.readConfig(), throwsStateError); + }); + }, + ); + + test( + 'transport loss after exposing a write retains uncertainty before disposal', + () async { + final failure = StateError('lost write response'); + final transport = _Transport( + ['9000', '9000', '01000001013F9000'], + onCommand: (command) async { + if (command == '00400100') throw failure; + }, ); - expect( - await AdminCardClient( - transport: _QueueApduTransport(['6D00']), - ).readCoreCommit(), - isNull, + await _prepared(transport, (client) async { + await expectLater( + client.configure(pin: '123456', ledOn: false), + throwsA(same(failure)), + ); + expect(client.lastProgress!.confirmedWrites, 0); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(transport.commands, hasLength(4)); + }); + }, + ); + + test( + 'cancellation drains the write and records exposed mutation without publishing success', + () async { + final started = Completer(); + final finish = Completer(); + final transport = _Transport( + ['9000', '9000', '01000001013F9000', '9000'], + onCommand: (command) async { + if (command == '00400100') { + started.complete(); + await finish.future; + } + }, ); + await _prepared(transport, (client) async { + final write = client.configure(pin: '123456', ledOn: false); + final rejected = expectLater(write, throwsStateError); + await started.future; + await expectLater(client.prepare(), throwsStateError); + await expectLater(client.readConfig(), throwsStateError); + client.cancelPendingOperations(); + finish.complete(); + await rejected; + expect(client.lastResponse, isNull); + expect(client.lastProgress!.confirmedWrites, 0); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(transport.commands, hasLength(4)); + }); }, ); - test('encodes admin PIN operations as UTF-8 bytes', () async { - final transport = _QueueApduTransport(['9000', '9000']); - final client = AdminCardClient(transport: transport); + test( + 'no-op patches keep profile evidence and never send a mutation', + () async { + final transport = _Transport([ + '9000', + '9000', + '01000001013F9000', + '9000', + '01000001013F9000', + ]); + await _prepared(transport, (client) async { + await client.configure(pin: '123456', ledOn: true); + expect(client.lastProgress!.confirmedWrites, 0); + expect(client.lastProgress!.reprobeRequired, isFalse); + expect(await client.readConfig(), hasLength(6)); + expect(transport.commands, [ + _select, + _verify, + '0042000000', + _select, + '0042000000', + ]); + }); + }, + ); + + test( + 'unknown feature bits prevent all patch writes instead of overwriting them', + () async { + final transport = _Transport(['9000', '9000', '0100000101809000']); + await _prepared(transport, (client) async { + await expectLater( + client.configure( + pin: '123456', + ledOn: false, + featureMask: 1, + featureValues: 1, + ), + _kind('UnsupportedProtocolVersion'), + ); + expect(client.lastProgress!.reprobeRequired, isFalse); + expect(transport.commands, [_select, _verify, '0042000000']); + }); + }, + ); - expect(await client.verifyPin('CanoKey密码'), isTrue); - await client.changePin('CanoKey密码'); + test( + 'modern SM2 writes signed big-endian values with explicit authentication', + () async { + final transport = _Transport([ + '9000', + '9000', + '00000009FFFFFFCA9000', + '9000', + '9000', + '00000009FFFFFFCA9000', + '9000', + ]); + await _prepared(transport, (client) async { + await client.writeSm2Config( + pin: '123456', + enabled: false, + curveId: -2147483648, + algoId: 2147483647, + ); + expect(transport.commands, [ + _select, + _verify, + '0011000000', + _select, + _verify, + '0011000000', + '0012000008800000007FFFFFFF', + ]); + expect(client.lastProgress!.reprobeRequired, isTrue); + }); + }, + ); - expect(transport.commands, [ - '002000000d43616e6f4b6579e5af86e7a081', - '002100000d43616e6f4b6579e5af86e7a081', - ]); - }); + test( + 'legacy SM2 uses explicit packed little-endian fields and retains enable flag', + () async { + final transport = _Transport([ + '9000', + '9000', + '0109000000D0FFFFFF9000', + '9000', + '9000', + '9000', + ]); + await _prepared(transport, (client) async { + await client.writeSm2Config( + pin: '123456', + enabled: false, + curveId: 10, + algoId: -48, + ); + expect(transport.commands, [ + '${_select}00', + '${_verify}00', + '0011000000', + '${_select}00', + '${_verify}00', + '0012000009000A000000D0FFFFFF00', + ]); + }, firmware: '3.0.3'); + }, + ); - test('rejects admin PINs that exceed short APDU length', () async { - final transport = _QueueApduTransport([]); - final client = AdminCardClient(transport: transport); - final pin = List.filled(64, '\u{1F600}').join(); + test('reserved SM2 identifiers never reach a write', () async { + final transport = _Transport(['9000', '9000', '00000009FFFFFFCA9000']); + await _prepared(transport, (client) async { + await expectLater( + client.writeSm2Config( + pin: '123456', + enabled: true, + curveId: 1, + algoId: -54, + ), + throwsArgumentError, + ); + expect(transport.commands, [_select, _verify, '0011000000']); + }); + }); - await expectLater(client.verifyPin(pin), throwsArgumentError); - await expectLater(client.changePin(pin), throwsArgumentError); - expect(transport.commands, isEmpty); + test('optional commit only falls back on unsupported feature', () async { + for (final response in ['6D00', '9000', '6162639000']) { + await _prepared(_Transport(['9000', response]), (client) async { + expect( + await client.readCoreCommit(), + response == '6162639000' ? 'abc' : null, + ); + }); + } + await _prepared(_Transport(['9000', '6982']), (client) async { + await expectLater( + client.readCoreCommit(), + _kind('SecurityStatusNotSatisfied'), + ); + }); }); test( - 'rejects invalid admin response data and configuration indexes', + 'NDEF reset and factory reset use distinct explicit authentication requirements', () async { - final client = AdminCardClient(transport: _QueueApduTransport([])); + await _prepared(_Transport(['9000', '9000', '9000']), (client) async { + await client.resetNdef(pin: '123456'); + expect(client.lastProgress!.confirmedWrites, 1); + }); + final transport = _Transport(['9000', '6985']); + await _prepared(transport, (client) async { + await expectLater( + client.factoryReset(), + _kind('ConditionsNotSatisfied'), + ); + expect(transport.commands, [_select, '00500000055245534554']); + expect(client.lastProgress!.reprobeRequired, isTrue); + }); + }, + ); - expect(() => client.writeConfigByte(-1, 0), throwsRangeError); - expect(() => client.writeConfigByte(1, 256), throwsRangeError); - expect( - AdminCardClient( - transport: _QueueApduTransport(['029000']), - ).readNfcEnabled(), - throwsFormatException, - ); - expect( - AdminCardClient( - transport: _QueueApduTransport(['019000']), - ).readStorageUsage(), - throwsFormatException, - ); + test( + 'malformed mutation acknowledgments and 6C responses are not replayed', + () async { + for (final response in ['019000', '6C00']) { + final transport = _Transport(['9000', '9000', response]); + await _prepared(transport, (client) async { + await expectLater( + client.setNfcEnabled(false, pin: '123456'), + throwsA(isA()), + ); + expect(transport.commands, [_select, _verify, '00140100']); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(client.lastProgress!.confirmedWrites, 0); + }); + } + }, + ); + + test( + 'shared lease invalidates PIV selection and all profiles after Admin writes', + () async { + final transport = _Transport([ + ..._probe('3.1.0'), + ..._probe('3.1.0'), + ..._probe('3.1.0'), + '9000', + '0600009000', + '01E00516E1531554E2E39000', + '9000', + 'AABB9000', + '9000', + '9000', + '01000001013F9000', + '9000', + ]); + await CardSessions().run((session) async { + session.bind(transport.transceive); + final admin = AdminCardClient( + transport: transport, + lease: session.lease, + ); + final other = AdminCardClient( + transport: transport, + lease: session.lease, + ); + final piv = PivCardClient(transport: transport, lease: session.lease); + await admin.prepare(); + await other.prepare(); + await piv.prepare(); + expect(await admin.readChipId(), 'AABB'); + final afterRead = transport.commands.length; + await expectLater(piv.readMetadata(0x80), throwsStateError); + await expectLater(piv.readVersion(), throwsStateError); + await expectLater(piv.readCertificate(5), throwsStateError); + expect(transport.commands, hasLength(afterRead)); + await admin.configure(pin: '123456', ledOn: false); + final afterWrite = transport.commands.length; + await expectLater(other.readConfig(), throwsStateError); + expect(transport.commands, hasLength(afterWrite)); + }); + }, + ); + + test( + 'profiles expire on session release and failed reprobe cannot restore them', + () async { + final transport = _Transport([..._probe('3.1.0'), '6F00']); + final client = AdminCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + await expectLater(client.prepare(), throwsA(isA())); + await expectLater(client.readConfig(), throwsStateError); + }); + await client.withSession(() async { + await expectLater(client.verifyPin('123456'), throwsStateError); + }); }, ); } -class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); +const _select = '00A4040005F000000000'; +const _verify = '0020000006313233343536'; +List _probe(String firmware) => [ + '9000', + '${hex.encode(utf8.encode(firmware))}9000', + '43616E6F4B65799000', + '010203049000', +]; +Matcher _kind(String kind) => throwsA( + isA().having((e) => e.details.kind, 'kind', kind), +); +Future _prepared( + _Transport transport, + Future Function(AdminCardClient) body, { + String firmware = '3.1.0', +}) async { + transport.responses.insertAll(0, _probe(firmware)); + final client = AdminCardClient(transport: transport); + return client.withSession(() async { + await client.prepare(); + expect(transport.commands, [ + '${_select}00', + '0031000000', + '0031010000', + '0032000000', + ]); + transport.commands.clear(); + return body(client); + }); +} +class _Transport implements ApduTransport { + _Transport(List responses, {this.onCommand}) + : responses = List.of(responses); + final Future Function(String)? onCommand; final List responses; final List commands = []; - int _responseIndex = 0; - @override - Future transceive(String capdu) async { - commands.add(capdu); - return responses[_responseIndex++]; + Future transceive(String command) async { + commands.add(command); + await onCommand?.call(command); + return responses.removeAt(0); } } diff --git a/test/helper/utils/applet_switches_test.dart b/test/helper/utils/applet_switches_test.dart index 807cf40..4c643fa 100644 --- a/test/helper/utils/applet_switches_test.dart +++ b/test/helper/utils/applet_switches_test.dart @@ -1,3 +1,7 @@ +@Tags(['native']) +library; + +import 'package:canokey_console/src/rust/frb_generated.dart'; import 'dart:convert'; import 'package:canokey_console/helper/utils/admin_card.dart'; @@ -9,17 +13,27 @@ import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); tearDown(() => SmartCard.connectionType = ConnectionType.none); test('legacy firmware does not read the feature configuration', () async { final transport = _Transport([ '9000', '${hex.encode(utf8.encode('3.0.0'))}9000', + '43616e6f4b65799000', + '010203049000', ]); - final status = await AppletSwitches.readStatus( - client: AdminCardClient(transport: transport), + final client = AdminCardClient(transport: transport); + final status = await client.withSession( + () => AppletSwitches.readStatus(client: client), ); - expect(transport.commands, ['00A4040005F000000000', '0031000000']); + expect(transport.commands, [ + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + ]); expect(status.functionSetVersion, FunctionSetVersion.v4); expect(status.featureSwitchesSupported, isFalse); expect(status.passEnabled, isTrue); @@ -33,14 +47,21 @@ void main() { final transport = _Transport([ '9000', '${hex.encode(utf8.encode('3.1.0'))}9000', + '43616e6f4b65799000', + '010203049000', + '9000', '01000001000d9000', ]); - final status = await AppletSwitches.readStatus( - client: AdminCardClient(transport: transport), + final client = AdminCardClient(transport: transport); + final status = await client.withSession( + () => AppletSwitches.readStatus(client: client), ); expect(transport.commands, [ - '00A4040005F000000000', + '00A4040005F00000000000', '0031000000', + '0031010000', + '0032000000', + '00A4040005F000000000', '0042000000', ]); expect(status.featureSwitchesSupported, isTrue); diff --git a/test/helper/utils/card_session_test.dart b/test/helper/utils/card_session_test.dart new file mode 100644 index 0000000..8b2b74c --- /dev/null +++ b/test/helper/utils/card_session_test.dart @@ -0,0 +1,249 @@ +import 'dart:async'; + +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:canokey_console/helper/utils/smartcard.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test( + 'lease resources release on rebind and despite a failing destructor', + () async { + final sessions = CardSessions(); + final released = []; + await expectLater( + sessions.run((session) async { + session.bind((_) async => '9000'); + session.lease.onClose(() => released.add('old')); + session.bind((_) async => '9000'); + expect(released, ['old']); + session.lease.onClose(() => released.add('new')); + session.lease.onClose(() => throw StateError('cleanup failed')); + }), + throwsStateError, + ); + expect(released, ['old', 'new']); + await sessions.run((_) async => released.add('next')); + expect(released.last, 'next'); + expect(sessions.isBusy, isFalse); + }, + ); + + test( + 'operation reservation excludes raw I/O and rebinding between APDUs', + () async { + final sessions = CardSessions(); + await sessions.run((session) async { + final commands = []; + session.bind((command) async { + commands.add(command); + return '9000'; + }); + final reservation = session.lease.beginOperation(); + await reservation.exchange('READ'); + expect(session.lease.beginOperation, throwsStateError); + await expectLater(session.lease.exchange('SELECT'), throwsStateError); + expect(() => session.bind((_) async => '9000'), throwsStateError); + await reservation.exchange('GET RESPONSE'); + reservation.close(); + reservation.close(); + await session.lease.exchange('NEXT'); + expect(commands, ['READ', 'GET RESPONSE', 'NEXT']); + }); + }, + ); + + test( + 'Admin authentication evidence follows selection and profile generations', + () async { + final sessions = CardSessions(); + await sessions.run((session) async { + session.bind((_) async => '9000'); + final lease = session.lease; + expect(lease.hasAdminAuthentication, isFalse); + lease.recordAdminAuthentication(); + expect(lease.hasAdminAuthentication, isTrue); + // Explicit invalidation, applet selection and profile invalidation + // each expire the recorded evidence. + lease.invalidateAdminAuthentication(); + expect(lease.hasAdminAuthentication, isFalse); + lease.recordAdminAuthentication(); + lease.willSelectApplet(); + expect(lease.hasAdminAuthentication, isFalse); + lease.recordAdminAuthentication(); + lease.invalidateProfileEvidence(); + expect(lease.hasAdminAuthentication, isFalse); + // A replaced lease carries no evidence at all. + lease.recordAdminAuthentication(); + session.bind((_) async => '9000'); + expect(session.lease.hasAdminAuthentication, isFalse); + }); + }, + ); + + test( + 'SmartCard scopes own and expire leases around complete callbacks', + () async { + SmartCard.connectionType = ConnectionType.ccid; + addTearDown(() => SmartCard.connectionType = ConnectionType.none); + final ready = Completer(); + final finish = Completer(); + late CardLease lease; + var nextStarted = false; + final first = SmartCard.process((_) async { + lease = SmartCard.currentLease; + ready.complete(); + await finish.future; + lease.check(); + }); + await ready.future; + final next = SmartCard.process((_) async { + nextStarted = true; + expect(() => lease.check(), throwsStateError); + SmartCard.currentLease.check(); + }); + await Future.delayed(Duration.zero); + expect(nextStarted, isFalse); + finish.complete(); + await Future.wait([first, next]); + expect(() => SmartCard.currentLease, throwsStateError); + expect(() => lease.check(), throwsStateError); + }, + ); + + test( + 'holds exclusivity across selection, authentication and publication', + () async { + final sessions = CardSessions(); + final selected = Completer(); + final proceed = Completer(); + final events = []; + late CardLease oldLease; + final first = sessions.run((session) async { + session.bind((command) async { + events.add(command); + return '9000'; + }); + oldLease = session.lease; + await oldLease.exchange('SELECT'); + selected.complete(); + await proceed.future; + await oldLease.exchange('VERIFY'); + await oldLease.exchange('READ'); + oldLease.check(); + events.add('publish'); + }); + await selected.future; + final second = sessions.run((session) async { + events.add('next'); + session.bind((_) async => '9000'); + expect(() => oldLease.check(), throwsStateError); + }); + await Future.delayed(Duration.zero); + expect(events, ['SELECT']); + proceed.complete(); + await Future.wait([first, second]); + expect(events, ['SELECT', 'VERIFY', 'READ', 'publish', 'next']); + expect(sessions.isBusy, isFalse); + }, + ); + + test( + 'disconnect rejects a late response and never switches its channel', + () async { + final sessions = CardSessions(); + final response = Completer(); + await sessions.run((session) async { + session.bind((_) => response.future); + final lease = session.lease; + final exchange = lease.exchange('READ'); + final rejected = expectLater(exchange, throwsStateError); + sessions.invalidate(); + response.complete('old device response'); + await rejected; + expect(() => lease.check(), throwsStateError); + session.bind((_) async => 'new device'); + expect(await session.lease.exchange('SELECT'), 'new device'); + expect(() => lease.check(), throwsStateError); + }); + }, + ); + + test('even an unawaited I/O drains before the next use case', () async { + final sessions = CardSessions(); + final response = Completer(); + final started = Completer(); + late Future rejected; + final first = sessions.run((session) async { + session.bind((_) => response.future); + rejected = expectLater(session.lease.exchange('READ'), throwsStateError); + started.complete(); + }); + await started.future; + var nextStarted = false; + final next = sessions.run((_) async => nextStarted = true); + await Future.delayed(Duration.zero); + expect(nextStarted, isFalse); + response.complete('9000'); + await Future.wait([first, next, rejected]); + expect(nextStarted, isTrue); + }); + + test('a transport error poisons the lease without replay', () async { + final sessions = CardSessions(); + final failure = TimeoutException('transport timeout'); + var calls = 0; + await sessions.run((session) async { + session.bind((_) async { + calls++; + throw failure; + }); + final lease = session.lease; + await expectLater(lease.exchange('WRITE'), throwsA(same(failure))); + await expectLater(lease.exchange('WRITE'), throwsStateError); + expect(calls, 1); + }); + }); + + test( + 'refuses overlapping I/O and rebind while a command is pending', + () async { + final sessions = CardSessions(); + final response = Completer(); + await sessions.run((session) async { + session.bind((_) => response.future); + final pending = session.lease.exchange('READ'); + await expectLater(session.lease.exchange('SELECT'), throwsStateError); + expect(() => session.bind((_) async => '9000'), throwsStateError); + response.complete('9000'); + await pending; + }); + }, + ); + + test( + 'bound UI callback retains ownership but cannot outlive its use case', + () async { + final sessions = CardSessions(); + final ready = Completer(); + final submitted = Completer(); + late Future Function() submit; + final useCase = sessions.run((session) async { + session.bind((_) async => '9000'); + submit = Zone.current.bindCallback(() async { + expect(sessions.current, same(session)); + await sessions.current!.lease.exchange('VERIFY'); + submitted.complete(); + }); + ready.complete(); + await submitted.future; + }); + await ready.future; + expect(sessions.current, isNull); + await submit(); + await useCase; + await expectLater(submit(), throwsStateError); + }, + ); +} diff --git a/test/helper/utils/ndef_card_test.dart b/test/helper/utils/ndef_card_test.dart index 2e329a1..f1cc5de 100644 --- a/test/helper/utils/ndef_card_test.dart +++ b/test/helper/utils/ndef_card_test.dart @@ -1,61 +1,41 @@ +@Tags(['native']) +library; + import 'dart:typed_data'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/ndef_card.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; -void main() { - group('NDEF binary APDUs', () { - test('builds short READ BINARY commands', () { - expect(NdefCardClient.readBinaryApdu(0x0102, 0xf0), '00B00102F0'); - }); +const _selectApplet = '00A4040007D2760000850101'; +const _selectCc = '00A4000C02E103'; +const _selectNdef = '00A4000C020001'; +const _readCc = '00B000000F'; +const _readNlen = '00B0000002'; - test('builds short UPDATE BINARY commands', () { - expect( - NdefCardClient.updateBinaryApdu(0, Uint8List.fromList([0x00, 0x11])), - '00D60000020011', - ); - }); +/// 15-byte capability container for a writable file of [fileSize] bytes. +List _capabilityContainer(int fileSize, {bool readOnly = false}) => [ + 0x00, 0x0f, 0x20, 0x00, 0xf0, 0x00, 0xf0, // + 0x04, 0x06, 0xe1, 0x04, + fileSize >> 8, fileSize & 0xff, 0x00, readOnly ? 0xff : 0x00, +]; - test('rejects APDUs outside the short command range', () { - expect(() => NdefCardClient.readBinaryApdu(0, 0), throwsRangeError); - expect( - () => NdefCardClient.updateBinaryApdu(0, Uint8List(256)), - throwsRangeError, - ); - }); - }); - test( - 'reads capability and message data through the injected transport', - () async { - final capability = [ - 0x00, - 0x0f, - 0x20, - 0x00, - 0xf0, - 0x00, - 0xf0, - 0x04, - 0x06, - 0xe1, - 0x04, - 0x00, - 0x05, - 0x00, - 0x00, - ]; - final transport = _QueueApduTransport([ - '9000', - '9000', - '${hex.encode(capability)}9000', - '9000', - '00039000', - '0102039000', - ]); - final client = NdefCardClient(transport: transport); +String _ok(List data) => '${hex.encode(data)}9000'; +void main() { + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); + + test('reads capability and message through the libcanokey machines', () async { + final cc = _capabilityContainer(5); // max message length 3 + final transport = _QueueApduTransport([ + '9000', '9000', _ok(cc), // read capability + '9000', '9000', _ok(cc), '9000', _ok([0, 3]), _ok([1, 2, 3]), // read + ]); + await _withClient(transport, (client) async { final data = await client.read(); expect(data, isNotNull); @@ -63,92 +43,149 @@ void main() { expect(data.readOnly, isFalse); expect(data.message, [1, 2, 3]); expect(transport.commands, [ - '00A4040007D2760000850101', - '00A4000C02E103', - '00B000000F', - '00A4000C020001', - '00B0000002', + _selectApplet, _selectCc, _readCc, + _selectApplet, _selectCc, _readCc, _selectNdef, _readNlen, '00B0000203', ]); - }, - ); + }); + }); + + test('reads an empty message without further message reads', () async { + final cc = _capabilityContainer(1024, readOnly: true); + final transport = _QueueApduTransport([ + '9000', '9000', _ok(cc), + '9000', '9000', _ok(cc), '9000', _ok([0, 0]), + ]); + await _withClient(transport, (client) async { + final data = await client.read(); - test( - 'writes messages in production-sized chunks and commits NLEN last', - () async { - final transport = _QueueApduTransport(List.filled(6, '9000')); - final client = NdefCardClient(transport: transport); - final message = Uint8List.fromList(List.generate(241, (index) => index)); + expect(data, isNotNull); + expect(data!.maxMessageLength, 1022); + expect(data.readOnly, isTrue); + expect(data.message, isEmpty); + expect(transport.commands, [ + _selectApplet, _selectCc, _readCc, + _selectApplet, _selectCc, _readCc, _selectNdef, _readNlen, + ]); + }); + }); + test('writes zero NLEN, 240-byte chunks and the real NLEN last', () async { + final transport = _QueueApduTransport(List.filled(6, '9000')); + final message = Uint8List.fromList(List.generate(241, (index) => index)); + await _withClient(transport, (client) async { expect(await client.write(message), isTrue); - expect(transport.commands[2], '00D60000020000'); - expect(transport.commands[3].startsWith('00D60002F0'), isTrue); - expect(transport.commands[4], '00D600F201F0'); - expect(transport.commands[5], '00D600000200F1'); - }, - ); - - test('reports unavailable and read-only NDEF applets', () async { - expect( - await NdefCardClient(transport: _QueueApduTransport(['6A82'])).read(), - isNull, - ); - expect( - await NdefCardClient( - transport: _QueueApduTransport(['6A82']), - ).write(Uint8List.fromList([1])), - isFalse, - ); - expect( - NdefCardClient( - transport: _QueueApduTransport(['9000', '9000', '6982']), - ).write(Uint8List.fromList([1])), - throwsA(isA()), - ); + expect(transport.commands, [ + _selectApplet, + _selectNdef, + '00D60000020000', + '00D60002F0${hex.encode(message.sublist(0, 240)).toUpperCase()}', + '00D600F201F0', + '00D600000200F1', + ]); + }); }); - test('rejects malformed capability and oversized message lengths', () async { - expect( - NdefCardClient( - transport: _QueueApduTransport([ - '9000', - '9000', - '${hex.encode(List.filled(15, 0))}9000', - ]), - ).read(), - throwsFormatException, - ); - - final capability = List.filled(15, 0) - ..[7] = 0x04 - ..[8] = 0x06 - ..[12] = 0x03; - expect( - NdefCardClient( - transport: _QueueApduTransport([ - '9000', - '9000', - '${hex.encode(capability)}9000', - '9000', - '00029000', - ]), - ).read(), - throwsFormatException, - ); + test('writes an empty message as two NLEN updates', () async { + final transport = _QueueApduTransport(List.filled(4, '9000')); + await _withClient(transport, (client) async { + expect(await client.write(Uint8List(0)), isTrue); + expect(transport.commands, [ + _selectApplet, + _selectNdef, + '00D60000020000', + '00D60000020000', + ]); + }); }); - test('rejects invalid short APDU ranges', () { - expect(() => NdefCardClient.readBinaryApdu(-1, 1), throwsRangeError); - expect( - () => NdefCardClient.updateBinaryApdu(0, Uint8List(0)), - throwsRangeError, - ); + test('reports an absent NDEF applet as unavailable', () async { + await _withClient(_QueueApduTransport(['6A82']), (client) async { + expect(await client.read(), isNull); + }); + await _withClient(_QueueApduTransport(['6A82']), (client) async { + expect(await client.write(Uint8List.fromList([1])), isFalse); + }); + }); + + test('maps a 6982 write rejection to NdefReadOnlyException', () async { + final transport = _QueueApduTransport(['9000', '9000', '6982']); + await _withClient(transport, (client) async { + await expectLater( + client.write(Uint8List.fromList([1])), + throwsA(isA()), + ); + // The failed write stopped at the zero-NLEN update; nothing is replayed. + expect(transport.commands, [ + _selectApplet, + _selectNdef, + '00D60000020000', + ]); + }); }); + + test('rejects a malformed capability container', () async { + final transport = _QueueApduTransport([ + '9000', '9000', _ok(List.filled(15, 0)), + ]); + await _withClient(transport, (client) async { + await expectLater( + client.read(), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidResponse') + .having((e) => e.details.phase, 'phase', 'Parsing')), + ); + }); + }); + + test('rejects an NLEN beyond the capability limit before reading', () async { + final cc = _capabilityContainer(3); // max message length 1 + final transport = _QueueApduTransport([ + '9000', '9000', _ok(cc), + '9000', '9000', _ok(cc), '9000', _ok([0, 2]), + ]); + await _withClient(transport, (client) async { + await expectLater( + client.read(), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidResponse')), + ); + expect(transport.commands, [ + _selectApplet, _selectCc, _readCc, + _selectApplet, _selectCc, _readCc, _selectNdef, _readNlen, + ]); + }); + }); + + test('rejects an oversized message before any I/O', () async { + final transport = _QueueApduTransport([]); + await _withClient(transport, (client) async { + await expectLater( + client.write(Uint8List(1023)), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidArgument')), + ); + expect(transport.commands, isEmpty); + }); + }); + + test('injected transports require an explicit session', () async { + final client = NdefCardClient(transport: _QueueApduTransport([])); + await expectLater(client.read(), throwsStateError); + }); +} + +Future _withClient( + _QueueApduTransport transport, + Future Function(NdefCardClient) action, +) { + final client = NdefCardClient(transport: transport); + return client.withSession(() => action(client)); } class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); + _QueueApduTransport(List responses) : responses = List.of(responses); final List responses; final List commands = []; diff --git a/test/helper/utils/oath_card_test.dart b/test/helper/utils/oath_card_test.dart index 0be6d41..f3dbe9d 100644 --- a/test/helper/utils/oath_card_test.dart +++ b/test/helper/utils/oath_card_test.dart @@ -1,132 +1,357 @@ +@Tags(['native']) +library; + +import 'dart:typed_data'; + import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/oath_card.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/models/oath.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; +import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; +const _selectCommand = '00A4040007A0000005272101'; + void main() { + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); + test('builds OATH operations and follows calculate-all pagination', () async { + // Every operation selects the applet before its target command. final transport = _QueueApduTransport([ - '79030600007101019000', - '9000', - '7605000000019000', - '9000', - '7101416102', - '7605000000019000', - '6985', + _modernSelection, + _modernSelection, '9000', // put + _modernSelection, '7605060000002A9000', // calculate + _modernSelection, '9000', // delete + _modernSelection, '710141760506000000019000', '6985', // calculateAll ]); - final client = OathCardClient(transport: transport); + await _withPreparedClient(transport, (client) async { + final selection = await client.select(); + expect(selection.version, OathVersion.v2); + expect(selection.requiresCode, isFalse); + expect(hex.encode(selection.salt!), '3132333435363738'); - final selection = await client.select(); - expect(selection.version, OathVersion.v2); - expect( await client.put( name: 'A', secretHex: '0102', type: OathType.totp, algorithm: OathAlgorithm.sha1, digits: 6, - ), - '9000', - ); - expect( - await client.calculate( + ); + expect(client.lastStatusWord, '9000'); + + final (digits, rawCode) = await client.calculate( name: 'A', type: OathType.totp, challengeHex: '0000000000000001', - ), - '7605000000019000', - ); - expect(await client.delete('A'), '9000'); - expect( - await client.calculateAll('0000000000000001'), - '7101417605000000019000', - ); + ); + expect((digits, rawCode), (6, 42)); - expect(transport.commands, [ - '00A4040007A0000005272101', - '0001000009710141730421060102', - '00A200010d71014174080000000000000001', - '0002000003710141', - '00A400010a74080000000000000001', - '00A50000FF', - '00A50000FF', - ]); + await client.delete('A'); + + final entries = await client.calculateAll('0000000000000001'); + expect(entries.single.name, 'A'); + expect(entries.single.digits, 6); + expect(entries.single.rawCode, 1); + + expect(transport.commands, [ + _selectCommand, + _selectCommand, '0001000009710141730421060102', + _selectCommand, '00A200010D71014174080000000000000001', + _selectCommand, '0002000003710141', + _selectCommand, '00A400010A74080000000000000001FF', '00A50000FF', + ]); + }); }); test('uses legacy commands and encodes a four-byte HOTP counter', () async { final transport = _QueueApduTransport([ - '9000', - '9000', - '7605000000019000', + '9000', // legacy select returns no fields + '9000', '9000', // put + '9000', '7605060000002A9000', // calculate ]); - final client = OathCardClient(transport: transport); - - expect((await client.select()).version, OathVersion.legacy); - await client.put( - name: 'A', - secretHex: '0102', - type: OathType.hotp, - algorithm: OathAlgorithm.sha1, - digits: 6, - requireTouch: true, - initialValue: 1, - ); - await client.calculate(name: 'A', type: OathType.hotp); - - expect(transport.commands[1], contains('7801027A0400000001')); - expect(transport.commands[2], startsWith('00040000')); - }); - - test('rejects invalid OATH challenge and counter sizes', () async { - final client = OathCardClient(transport: _QueueApduTransport([])); + await _withPreparedClient(transport, (client) async { + expect((await client.select()).version, OathVersion.legacy); - expect( - () => client.calculate( - name: 'A', - type: OathType.totp, - challengeHex: '00', - ), - throwsArgumentError, - ); - expect(() => client.calculateAll('00'), throwsArgumentError); - expect( - () => client.put( + await client.put( name: 'A', secretHex: '0102', type: OathType.hotp, algorithm: OathAlgorithm.sha1, digits: 6, - initialValue: 0x100000000, - ), - throwsRangeError, - ); + requireTouch: true, + initialValue: 1, + ); + await client.calculate(name: 'A', type: OathType.hotp); + + // Legacy firmware selects the 1.3 instruction set with explicit Le. + expect(transport.commands, [ + '${_selectCommand}00', + '${_selectCommand}00', + '00010000127101417304110601027801027A040000000100', + '${_selectCommand}00', + '000400000371014100', + ]); + }, firmware: '1.3.0'); + }); + + test('rejects invalid OATH challenge and counter sizes', () async { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, (client) async { + expect( + () => client.calculate( + name: 'A', + type: OathType.totp, + challengeHex: '00', + ), + throwsArgumentError, + ); + expect(() => client.calculateAll('00'), throwsArgumentError); + expect( + () => client.put( + name: 'A', + secretHex: '0102', + type: OathType.hotp, + algorithm: OathAlgorithm.sha1, + digits: 6, + initialValue: 0x100000000, + ), + throwsRangeError, + ); + expect(transport.commands, isEmpty); + }); + }); + + test('validates an access code within the operation', () async { + final transport = _QueueApduTransport([ + _protectedSelection, // select + _protectedSelection, // validate's own select + '7514CAA1346E39058DD36ED76FB49053E14F66CE428B9000', + ]); + await _withPreparedClient(transport, (client) async { + client.challengeGenerator = + () => Uint8List.fromList('HHHHHHHH'.codeUnits); + final selection = await client.select(); + expect(selection.version, OathVersion.v2); + expect(selection.requiresCode, isTrue); + + // Key "KKKKKKKKKKKKKKKK", host challenge "HHHHHHHH", card challenge + // "CCCCCCCC"; HMAC known-answer values from canokey-oath transcripts. + await client.validate(Uint8List.fromList(List.filled(16, 0x4B))); + expect(client.lastStatusWord, '9000'); + expect(transport.commands, [ + _selectCommand, + _selectCommand, + '00A3000020' + '75140DE0BAE281BA21980E7692C934529F0F61111651' + '74084848484848484848', + ]); + }); + }); + + test('reports an incorrect access code with its status word', () async { + final transport = _QueueApduTransport([_protectedSelection, '6A80']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.validate(Uint8List.fromList(List.filled(16, 0x4B))), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'AuthenticationFailed') + .having((e) => e.details.phase, 'phase', 'Authentication')), + ); + expect(client.lastStatusWord, '6A80'); + }); + }); + + test('a protected applet rejects access-less operations before the target', + () async { + final transport = _QueueApduTransport([_protectedSelection]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.delete('A'), + throwsA(isA().having( + (e) => e.details.kind, 'kind', 'SecurityStatusNotSatisfied')), + ); + // The operation selected the applet, then failed before the delete. + expect(transport.commands, [_selectCommand]); + }); }); - test('detects OATH v1 and preserves terminal list errors', () async { - final transport = _QueueApduTransport(['79030505059000', '6A80']); - final client = OathCardClient(transport: transport); + test('decodes code, HOTP and touch markers in calculate-all', () async { + final transport = _QueueApduTransport([ + _modernSelection, + '71014176050600000001' + '710142770106' + '7101437C0106' + '9000', + '6985', + ]); + await _withPreparedClient(transport, (client) async { + final entries = await client.calculateAll('0000000000000001'); + expect(entries, hasLength(3)); + expect(entries[0].name, 'A'); + expect(entries[0].rawCode, 1); + expect(entries[1].name, 'B'); + expect(entries[1].isHotp, isTrue); + expect(entries[1].rawCode, isNull); + expect(entries[2].name, 'C'); + expect(entries[2].requiresTouch, isTrue); + expect(transport.commands, [ + _selectCommand, + '00A400010A74080000000000000001FF', + '00A50000FF', + ]); + }); + }); - expect((await client.select()).version, OathVersion.v1); - expect(await client.calculateAll('0000000000000001'), '6A80'); + test('surfaces a duplicate credential name as 6985', () async { + final transport = _QueueApduTransport([_modernSelection, '6985']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.put( + name: 'A', + secretHex: '0102', + type: OathType.totp, + algorithm: OathAlgorithm.sha1, + digits: 6, + ), + throwsA(isA() + .having((e) => e.details.statusWord, 'statusWord', 0x6985)), + ); + expect(client.lastStatusWord, '6985'); + }); }); - test('stops pagination on an empty successful response', () async { - final transport = _QueueApduTransport(['7101416102', '9000']); - final client = OathCardClient(transport: transport); + test('set-default follows the modern two-slot dialect', () async { + final transport = _QueueApduTransport([_modernSelection, '9000']); + await _withPreparedClient(transport, (client) async { + await client.setDefault(name: 'test', slot: 1, appendEnter: true); + expect(client.lastStatusWord, '9000'); + expect(transport.commands, [ + _selectCommand, + '0055020106710474657374', + ]); + }); + }); + + test('set-default validates the access code within the operation', () async { + final transport = _QueueApduTransport([ + _protectedSelection, + '7514CAA1346E39058DD36ED76FB49053E14F66CE428B9000', + '9000', + ]); + await _withPreparedClient(transport, (client) async { + client.challengeGenerator = + () => Uint8List.fromList('HHHHHHHH'.codeUnits); + await client.setDefault( + name: 'test', + slot: 0, + appendEnter: false, + key: Uint8List.fromList(List.filled(16, 0x4B)), + ); + expect(transport.commands, [ + _selectCommand, + '00A3000020' + '75140DE0BAE281BA21980E7692C934529F0F61111651' + '74084848484848484848', + '0055010006710474657374', + ]); + }); + }); + + test('set-default on legacy firmware uses the zero-P1/P2 dialect', () async { + final transport = _QueueApduTransport(['9000', '9000']); + await _withPreparedClient(transport, (client) async { + await client.setDefault(name: 'test', slot: 0, appendEnter: false); + // Legacy firmware selects the 1.3 instruction set and the command + // carries an explicit trailing Le. + expect(transport.commands, [ + '${_selectCommand}00', + '005500000671047465737400', + ]); + }, firmware: '1.3.0'); + }); + + test('set-default on legacy firmware rejects long slots and enter before I/O', + () async { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, (client) async { + for (final (slot, appendEnter) in [(1, false), (0, true)]) { + await expectLater( + client.setDefault( + name: 'test', + slot: slot, + appendEnter: appendEnter, + ), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidArgument') + .having((e) => e.details.phase, 'phase', 'Construction') + .having((e) => e.exchangeAttempted, 'exchangeAttempted', false)), + ); + } + expect(transport.commands, isEmpty); + expect(client.lastStatusWord, isNull); + }, firmware: '1.3.0'); + }); + + test('set-default maps a missing name (6984) to NotFound', () async { + final transport = _QueueApduTransport([_modernSelection, '6984']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.setDefault(name: 'test', slot: 0, appendEnter: false), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'NotFound') + .having((e) => e.details.statusWord, 'statusWord', 0x6984)), + ); + expect(client.lastStatusWord, '6984'); + }); + }); + + test('set-default rejects unknown slots', () async { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, (client) async { + expect( + () => client.setDefault(name: 'test', slot: 2, appendEnter: false), + throwsRangeError, + ); + expect(transport.commands, isEmpty); + }); + }); +} + +const _modernSelection = '7903060000710831323334353637389000'; +const _protectedSelection = '790306000071083132333435363738' + '74084343434343434343' + '7B0101' + '9000'; - expect( - await client.calculateAll('0000000000000001'), - '7101419000', - ); +Future _withPreparedClient( + _QueueApduTransport transport, + Future Function(OathCardClient) action, { + String firmware = '3.1.0', +}) async { + transport.responses.insertAll(0, [ + '9000', + '${hex.encode(firmware.codeUnits)}9000', + '43616E6F4B65799000', + '010203049000', + ]); + final client = OathCardClient(transport: transport); + return client.withSession(() async { + await client.prepare(); expect(transport.commands, [ - '00A400010a74080000000000000001', - '00A50000FF', + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', ]); + transport.commands.clear(); + return action(client); }); } class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); + _QueueApduTransport(List responses) : responses = List.of(responses); final List responses; final List commands = []; diff --git a/test/helper/utils/openpgp_card_test.dart b/test/helper/utils/openpgp_card_test.dart index 107afe2..d96f3da 100644 --- a/test/helper/utils/openpgp_card_test.dart +++ b/test/helper/utils/openpgp_card_test.dart @@ -1,48 +1,74 @@ +@Tags(['native']) +library; + import 'dart:convert'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/openpgp_card.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/models/openpgp.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('uses the injected transport and follows GET RESPONSE', () async { - final transport = _QueueApduTransport(['6102', '9000']); - final client = OpenPgpCardClient(transport: transport); - - await client.select(); + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); - expect(transport.commands, ['00A4040006D27600012401', '00C0000002']); - expect(client.lastStatusWord, '9000'); + test('every operation SELECTs through libcanokey and follows GET RESPONSE', + () async { + final transport = _QueueApduTransport(['6102', '9000', '6A82']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.verifyAdminPin('12345678'), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'NotFound', + ), + ), + ); + // The upstream engine owns continuation and Le correction. + expect(transport.commands, [ + '00A4040006D27600012401', + '00C0000002', + '00200083083132333435363738', + ]); + }); }); - test('changes the user PIN through the injected transport', () async { - final transport = _QueueApduTransport(['9000', '9000']); + test('operations require an explicitly prepared lease', () async { + final transport = _QueueApduTransport([]); final client = OpenPgpCardClient(transport: transport); - - final changed = await client.changeUserPin('123456', '654321'); - - expect(changed, isTrue); - expect(transport.commands, [ - '00A4040006D27600012401', - '002400810c313233343536363534333231', - ]); + await client.withSession(() async { + await expectLater(client.readCardInfo(), throwsStateError); + await expectLater( + client.changeUserPin('123456', '654321'), + throwsStateError, + ); + }); + expect(transport.commands, isEmpty); }); - test('parses card info with optional legacy data objects missing', () async { + test('reads card info with optional legacy data objects missing', () async { const aid = 'D2760001240103040000010203040000'; const application = '4F10${aid}C40700000000030303'; final transport = _QueueApduTransport([ '9000', '${application}9000', + '9000', '6A88', + '9000', '6A88', + '9000', '6A88', ]); - final client = OpenPgpCardClient(transport: transport); - final info = await client.readCardInfo(); + final info = await _withPreparedClient( + transport, + (client) => client.readCardInfo(), + ); expect(info.version, '3.4'); expect(info.serialNumber, '01020304'); @@ -51,6 +77,16 @@ void main() { expect(info.keySlots.keys, containsAll(OpenPgpKeyType.values)); expect(info.keySlots.values.every((slot) => !slot.hasKey), isTrue); expect(info.touchCacheTime, isNull); + expect(transport.commands, [ + '00A4040006D27600012401', + '00CA006E00', + '00A4040006D27600012401', + '00CA006500', + '00A4040006D27600012401', + '00CA5F5000', + '00A4040006D27600012401', + '00CA010200', + ]); }); test('parses current card data, key metadata, and touch policies', () async { @@ -74,12 +110,18 @@ void main() { final transport = _QueueApduTransport([ '9000', '${hex.encode(application)}9000', + '9000', '${hex.encode(holder)}9000', + '9000', '${hex.encode(utf8.encode('https://example.test/key'))}9000', + '9000', '0F9000', ]); - final info = await OpenPgpCardClient(transport: transport).readCardInfo(); + final info = await _withPreparedClient( + transport, + (client) => client.readCardInfo(), + ); expect(info.manufacturer, 'Yubico'); expect(info.cardHolder, 'Alice Example'); @@ -101,41 +143,148 @@ void main() { expect(info.keySlots[OpenPgpKeyType.authentication]!.touchFixed, isTrue); }); - test('builds OpenPGP administration operations', () async { - final transport = _QueueApduTransport(List.filled(20, '9000')); - final client = OpenPgpCardClient(transport: transport); + test('legacy firmware gates the touch cache read before any exchange', + () async { + const aid = 'D2760001240103040000010203040000'; + const application = '4F10${aid}C40700000000030303'; + final transport = _QueueApduTransport([ + '9000', + '${application}9000', + '9000', + '6A88', + '9000', + '6A88', + ]); - expect(await client.setResetCode('12345678', '12345678'), isTrue); - expect(await client.setPinRetries('12345678', 3, 3, 3), isTrue); - expect(await client.setSignaturePinPolicy('12345678', true), isTrue); - expect(await client.unblockUserPinWithAdmin('12345678', '123456'), isTrue); - expect( - await client.unblockUserPinWithResetCode('12345678', '123456'), - isTrue, - ); - expect( - await client.setTouchPolicy( - OpenPgpKeyType.signature, - OpenPgpTouchPolicy.on, - '12345678', - ), - isTrue, + final info = await _withPreparedClient( + transport, + (client) => client.readCardInfo(), + firmware: '1.3.0', ); - expect(await client.setTouchCacheTime('12345678', 15), isTrue); + expect(info.touchCacheTime, isNull); + // Legacy explicit Le applies to SELECT; the gated 0102 read performs no I/O. + expect(transport.commands, [ + '00A4040006D2760001240100', + '00CA006E00', + '00A4040006D2760001240100', + '00CA006500', + '00A4040006D2760001240100', + '00CA5F5000', + ]); + }); + + test('changes the user PIN through libcanokey', () async { + final transport = _QueueApduTransport(['9000', '9000']); + await _withPreparedClient(transport, (client) async { + expect(await client.changeUserPin('123456', '654321'), isTrue); + expect(client.lastStatusWord, '9000'); + expect(transport.commands, [ + '00A4040006D27600012401', + '002400810C313233343536363534333231', + ]); + }); + }); + + test('changes the admin PIN through libcanokey', () async { + final transport = _QueueApduTransport(['9000', '9000']); + await _withPreparedClient(transport, (client) async { + expect(await client.changeAdminPin('12345678', '87654321'), isTrue); + expect(transport.commands, [ + '00A4040006D27600012401', + '002400831031323334353637383837363534333231', + ]); + }); + }); + + test('credential rejections keep the status word and retries', () async { + final transport = _QueueApduTransport([ + '9000', '63C2', '9000', '6983', '9000', '6982', '9000', '9000', + ]); + await _withPreparedClient(transport, (client) async { + expect(await client.changeUserPin('123456', '654321'), isFalse); + expect(client.lastStatusWord, '63C2'); + expect(await client.changeAdminPin('12345678', '87654321'), isFalse); + expect(client.lastStatusWord, '6983'); + expect(await client.verifyAdminPin('12345678'), isFalse); + expect(client.lastStatusWord, '6982'); + expect(await client.verifyAdminPin('12345678'), isTrue); + expect(transport.commands, [ + '00A4040006D27600012401', + '002400810C313233343536363534333231', + '00A4040006D27600012401', + '002400831031323334353637383837363534333231', + '00A4040006D27600012401', + '00200083083132333435363738', + '00A4040006D27600012401', + '00200083083132333435363738', + ]); + }); + }); + + test('invalid credential lengths fail before any exchange', () async { + final transport = _QueueApduTransport(['9000', '9000']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.changeUserPin('12345', '654321'), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidPin', + ), + ), + ); + expect(transport.commands, isEmpty); + expect(await client.changeUserPin('123456', '654321'), isTrue); + }); + }); + + test('builds OpenPGP administration operations', () async { + final transport = _QueueApduTransport(List.filled(23, '9000')); + await _withPreparedClient(transport, (client) async { + expect(await client.setResetCode('12345678', '12345678'), isTrue); + expect(await client.setResetCode('12345678', ''), isTrue); + expect(await client.setPinRetries('12345678', 3, 3, 3), isTrue); + expect(await client.setSignaturePinPolicy('12345678', true), isTrue); + expect( + await client.unblockUserPinWithAdmin('12345678', '123456'), + isTrue, + ); + expect( + await client.unblockUserPinWithResetCode('12345678', '123456'), + isTrue, + ); + expect( + await client.setTouchPolicy( + OpenPgpKeyType.signature, + OpenPgpTouchPolicy.on, + '12345678', + ), + isTrue, + ); + expect(await client.setTouchCacheTime('12345678', 15), isTrue); + }); + + final commands = transport.commands; + expect(commands, contains('00DA00D3083132333435363738')); + // Clearing the reset code is a dataless PUT DATA. + expect(commands, contains('00DA00D3')); + expect(commands, contains('00F2000003030303')); + expect(commands, contains('00DA00C40100')); + expect(commands, contains('002C028106313233343536')); expect( - transport.commands.any((command) => command.startsWith('00DA00d3')), - isTrue, - ); - expect(transport.commands, contains('00F2000003030303')); - expect(transport.commands, contains('00DA00c40100')); - expect(transport.commands, contains('002C028106313233343536')); - expect( - transport.commands, - contains('002C00810e3132333435363738313233343536'), + commands, + contains('002C00810E3132333435363738313233343536'), ); - expect(transport.commands, contains('00DA00d6020120')); - expect(transport.commands, contains('00DA0102010f')); + expect(commands, contains('00DA00D6020120')); + expect(commands, contains('00DA0102010F')); + // Every PW3-protected operation explicitly verifies after its own SELECT; + // only the reset-code unblock skips verification. + final verifyCount = commands + .where((command) => command == '00200083083132333435363738') + .length; + expect(verifyCount, 7); }); test( @@ -156,35 +305,152 @@ void main() { for (final operation in operations) { for (final failure in ['6982', '63C2', '6983']) { final transport = _QueueApduTransport(['9000', failure]); - final client = OpenPgpCardClient(transport: transport); - expect(await operation(client), isFalse); - expect(transport.commands, [ - '00A4040006D27600012401', - '00200083083132333435363738', - ]); - expect(client.lastStatusWord, failure); + await _withPreparedClient(transport, (client) async { + expect(await operation(client), isFalse); + expect(transport.commands, [ + '00A4040006D27600012401', + '00200083083132333435363738', + ]); + expect(client.lastStatusWord, failure); + }); } + // Unexpected target-stage statuses are protocol failures, not a + // credential result; the status word is still preserved. final transport = _QueueApduTransport(['9000', '9000', '6581']); - final client = OpenPgpCardClient(transport: transport); - expect(await operation(client), isFalse); - expect(client.lastStatusWord, '6581'); - expect(transport.commands, hasLength(3)); + await _withPreparedClient(transport, (client) async { + await expectLater( + operation(client), + throwsA( + isA().having( + (e) => e.details.statusWord, + 'statusWord', + 0x6581, + ), + ), + ); + expect(client.lastStatusWord, '6581'); + expect(transport.commands, hasLength(3)); + }); } }, ); - test('changes the admin PIN and uses extended APDU lengths', () async { - final transport = _QueueApduTransport(List.filled(5, '9000')); - final client = OpenPgpCardClient(transport: transport); + test('administrative inputs are validated before any exchange', () async { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, (client) async { + // Reset codes must satisfy the 8..64 administrative length. + await expectLater( + client.setResetCode('12345678', List.filled(65, 'A').join()), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidPin', + ), + ), + ); + // Retry limits are exactly three values in 1..15. + await expectLater( + client.setPinRetries('12345678', 0, 3, 3), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidArgument', + ), + ), + ); + // A short reset code fails construction without I/O. + await expectLater( + client.unblockUserPinWithResetCode('123456', '654321'), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidPin', + ), + ), + ); + }); + expect(transport.commands, isEmpty); + }); - expect(await client.changeAdminPin('12345678', '87654321'), isTrue); - expect( - await client.setResetCode('12345678', List.filled(256, 'A').join()), - isTrue, - ); + test('legacy firmware gates UIF and retry-limit writes before any exchange', + () async { + for (final firmware in ['1.3.0', '1.6.0']) { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, firmware: firmware, (client) async { + if (firmware == '1.3.0') { + await expectLater( + client.setTouchPolicy( + OpenPgpKeyType.signature, + OpenPgpTouchPolicy.on, + '12345678', + ), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'UnsupportedFeature', + ), + ), + ); + await expectLater( + client.setTouchCacheTime('12345678', 15), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'UnsupportedFeature', + ), + ), + ); + } else { + // Retry-limit configuration exists from firmware 3.1.0. + await expectLater( + client.setPinRetries('12345678', 3, 3, 3), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'UnsupportedFeature', + ), + ), + ); + } + }); + expect(transport.commands, isEmpty); + } + }); - expect(transport.commands[1], startsWith('0024008310')); - expect(transport.commands[4], startsWith('00DA00d3000100')); + test('reset-code unblock maps credential failures without a verify', () async { + final transport = _QueueApduTransport([ + '9000', '63C2', '9000', '6983', '9000', '9000', + ]); + await _withPreparedClient(transport, (client) async { + expect( + await client.unblockUserPinWithResetCode('24682468', '654321'), + isFalse, + ); + expect(client.lastStatusWord, '63C2'); + expect( + await client.unblockUserPinWithResetCode('24682468', '654321'), + isFalse, + ); + expect(client.lastStatusWord, '6983'); + expect( + await client.unblockUserPinWithResetCode('24682468', '654321'), + isTrue, + ); + expect(transport.commands, [ + '00A4040006D27600012401', + '002C00810E3234363832343638363534333231', + '00A4040006D27600012401', + '002C00810E3234363832343638363534333231', + '00A4040006D27600012401', + '002C00810E3234363832343638363534333231', + ]); + }); }); } @@ -193,8 +459,35 @@ List _tlv(int tag, List value) { return [...tagBytes, value.length, ...value]; } +const _probeCommands = [ + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', +]; + +Future _withPreparedClient( + _QueueApduTransport transport, + Future Function(OpenPgpCardClient) action, { + String firmware = '3.1.0', +}) async { + transport.responses.insertAll(0, [ + '9000', + '${hex.encode(firmware.codeUnits)}9000', + '43616E6F4B65799000', + '010203049000', + ]); + final client = OpenPgpCardClient(transport: transport); + return client.withSession(() async { + await client.prepare(); + expect(transport.commands, _probeCommands); + transport.commands.clear(); + return action(client); + }); +} + class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); + _QueueApduTransport(List responses) : responses = List.of(responses); final List responses; final List commands = []; diff --git a/test/helper/utils/pass_card_test.dart b/test/helper/utils/pass_card_test.dart index d5c47a7..3dc9f47 100644 --- a/test/helper/utils/pass_card_test.dart +++ b/test/helper/utils/pass_card_test.dart @@ -1,73 +1,353 @@ +@Tags(['native']) +library; + +import 'dart:convert'; + +import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/helper/utils/pass_card.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/models/pass.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; +import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('configures and reads Pass slots through the injected transport', - () async { - final transport = _QueueApduTransport(['9000', '0201009000']); - final client = PassCardClient(transport: transport); + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); - expect( - await client.setSlot(1, PassSlotType.static, 'test', true), - isTrue, - ); - final slots = await client.readSlots(); + test( + 'reads typed slots, preserving OATH references and unknown states', + () async { + final transport = _Transport([ + '9000', + // Short = static with Enter; long = OATH credential "test" without Enter. + '0201010474657374009000', + '9000', + // Short = unknown type 0x7F carried verbatim; long = off. + '7F009000', + ]); + await _prepared(transport, (client) async { + final slots = await client.readSlots(); + expect(slots, hasLength(2)); + expect(slots[0].type, PassSlotType.static); + expect(slots[0].withEnter, isTrue); + expect(slots[1].type, PassSlotType.oath); + expect(slots[1].name, 'test'); + expect(slots[1].withEnter, isFalse); - expect(transport.commands, [ - '004401000702047465737401', - '0043000000', - ]); - expect(slots, hasLength(2)); - expect(slots.first.type, PassSlotType.static); - expect(slots.first.withEnter, isTrue); - expect(slots.last.type, PassSlotType.none); + final unknown = await client.readSlots(); + expect(unknown[0].type, PassSlotType.unknown); + expect(unknown[1].type, PassSlotType.none); + + // The unprotected read SELECTs but never verifies a PIN. + expect(transport.commands, [ + _select, + '0043000000', + _select, + '0043000000', + ]); + }); + }, + ); + + test('malformed slot dumps fail instead of fabricating slots', () async { + final transport = _Transport(['9000', '029000']); + await _prepared(transport, (client) async { + await expectLater(client.readSlots(), _kind('InvalidResponse')); + expect(transport.commands, [_select, '0043000000']); + }); + }); + + test( + 'static writes authenticate per request and invalidate profile evidence', + () async { + final transport = _Transport(['9000', '9000', '9000']); + await _prepared(transport, (client) async { + expect( + await client.setSlot( + 1, + PassSlotType.static, + 'pw', + true, + pin: '123456', + ), + isTrue, + ); + expect(client.lastResponse, '9000'); + expect(client.lastProgress!.confirmedWrites, 1); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(transport.commands, [_select, _verify, '00440100050202707701']); + // The write invalidates profile evidence until an explicit reprobe. + await expectLater(client.readSlots(), throwsStateError); + }); + }, + ); + + test( + 'off and HMAC-SHA1 writes re-probe after each profile-invalidating slot', + () async { + final transport = _Transport([ + '9000', + '9000', + '9000', + ..._probeResponses('3.1.0'), + '9000', + '9000', + '9000', + ]); + await _prepared(transport, (client) async { + expect( + await client.setSlot(2, PassSlotType.none, '', false, pin: '123456'), + isTrue, + ); + expect(transport.commands, [_select, _verify, '004402000100']); + transport.commands.clear(); + + await client.prepare(); + expect(transport.commands, [ + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + ]); + transport.commands.clear(); + + final key = List.filled(20, 0x09); + expect( + await client.setSlot( + 1, + PassSlotType.hmacSha1, + hex.encode(key), + false, + pin: '123456', + ), + isTrue, + ); + expect(transport.commands, [ + _select, + _verify, + '00440100160314${hex.encode(key)}', + ]); + }); + }, + ); + + test( + 'credential rejections keep the status word and never reach the write', + () async { + final transport = _Transport(['9000', '63C2', '9000', '6983']); + await _prepared(transport, (client) async { + expect( + await client.setSlot( + 1, + PassSlotType.static, + 'pw', + false, + pin: '123456', + ), + isFalse, + ); + expect(client.lastResponse, '63C2'); + expect( + await client.setSlot( + 1, + PassSlotType.static, + 'pw', + false, + pin: '123456', + ), + isFalse, + ); + expect(client.lastResponse, '6983'); + expect(transport.commands, [_select, _verify, _select, _verify]); + }); + }, + ); + + test( + 'a full slot store keeps its status word and is never replayed', + () async { + final transport = _Transport(['9000', '9000', '6A84']); + await _prepared(transport, (client) async { + await expectLater( + client.setSlot(1, PassSlotType.static, 'pw', false, pin: '123456'), + _kind('UnexpectedStatusWord'), + ); + expect(client.lastResponse, '6A84'); + expect(client.lastProgress!.confirmedWrites, 0); + expect(client.lastProgress!.reprobeRequired, isTrue); + expect(transport.commands, [_select, _verify, '00440100050202707700']); + }); + }, + ); + + test('invalid slots, types and secrets fail before any I/O', () async { + final transport = _Transport([]); + await _prepared(transport, (client) async { + await expectLater( + client.setSlot(0, PassSlotType.none, '', false, pin: '123456'), + throwsArgumentError, + ); + await expectLater( + client.setSlot(3, PassSlotType.none, '', false, pin: '123456'), + throwsArgumentError, + ); + await expectLater( + client.setSlot( + 1, + PassSlotType.static, + 'pässword', + false, + pin: '123456', + ), + throwsArgumentError, + ); + await expectLater( + client.setSlot(1, PassSlotType.static, 'x' * 33, false, pin: '123456'), + throwsArgumentError, + ); + await expectLater( + client.setSlot( + 1, + PassSlotType.hmacSha1, + 'zz' * 20, + false, + pin: '123456', + ), + throwsArgumentError, + ); + await expectLater( + client.setSlot( + 1, + PassSlotType.hmacSha1, + '09' * 19, + false, + pin: '123456', + ), + throwsArgumentError, + ); + await expectLater( + client.setSlot(1, PassSlotType.oath, '', false, pin: '123456'), + throwsArgumentError, + ); + await expectLater( + client.setSlot(1, PassSlotType.unknown, '', false, pin: '123456'), + throwsArgumentError, + ); + expect(transport.commands, isEmpty); + }); }); - test('encodes empty and HMAC-SHA1 slots and reports card errors', () async { - final transport = _QueueApduTransport(['9000', '6982']); + test( + 'a pass probe after Admin verification ends the shared session evidence', + () async { + final transport = _Transport([ + ..._probeResponses('3.1.0'), + '9000', + '9000', + ..._probeResponses('3.1.0'), + '9000', + '9000', + '9000', + ]); + await CardSessions().run((session) async { + session.bind(transport.transceive); + final admin = AdminCardClient( + transport: transport, + lease: session.lease, + ); + final pass = PassCardClient(transport: transport, lease: session.lease); + await admin.prepare(); + expect(await admin.verifyPin('123456'), isTrue); + // The pass client's own probe re-selects Admin on the shared lease, + // which ends the recorded session; the protected write falls back to + // its explicit per-request PIN. + await pass.prepare(); + expect( + await pass.setSlot(1, PassSlotType.static, 'pw', true, pin: '123456'), + isTrue, + ); + expect(transport.commands, [ + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + _select, + _verify, + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + _select, + _verify, + '00440100050202707701', + ]); + }); + }, + ); + + test('operations require an explicitly prepared lease', () async { + final transport = _Transport([]); final client = PassCardClient(transport: transport); - const secret = '0000000000000000000000000000000000000000'; - - expect(await client.setSlot(2, PassSlotType.none, '', false), isTrue); - expect( - await client.setSlot(1, PassSlotType.hmacSha1, secret, false), - isFalse, - ); - expect(client.lastStatusWord, '6982'); - expect(transport.commands[0], '004402000100'); - expect(transport.commands[1], '00440100160314$secret'); - expect( - () => client.setSlot(1, PassSlotType.oath, '', false), - throwsArgumentError, - ); + await client.withSession(() async { + await expectLater(client.readSlots(), throwsStateError); + await expectLater( + client.setSlot(1, PassSlotType.none, '', false, pin: '123456'), + throwsStateError, + ); + }); + expect(transport.commands, isEmpty); }); +} - test('rejects invalid slots and non-ASCII static passwords', () async { - final client = PassCardClient(transport: _QueueApduTransport([])); - - expect( - () => client.setSlot(0, PassSlotType.none, '', false), - throwsArgumentError, - ); - expect( - () => client.setSlot(1, PassSlotType.static, 'pässword', false), - throwsArgumentError, - ); +const _select = '00A4040005F000000000'; +const _verify = '0020000006313233343536'; + +List _probeResponses(String firmware) => [ + '9000', + '${hex.encode(utf8.encode(firmware))}9000', + '43616E6F4B65799000', + '010203049000', +]; + +Matcher _kind(String kind) => throwsA( + isA().having((e) => e.details.kind, 'kind', kind), +); + +Future _prepared( + _Transport transport, + Future Function(PassCardClient) body, { + String firmware = '3.1.0', +}) async { + transport.responses.insertAll(0, _probeResponses(firmware)); + final client = PassCardClient(transport: transport); + return client.withSession(() async { + await client.prepare(); + expect(transport.commands, [ + '${_select}00', + '0031000000', + '0031010000', + '0032000000', + ]); + transport.commands.clear(); + return body(client); }); } -class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); +class _Transport implements ApduTransport { + _Transport(List responses) : responses = List.of(responses); final List responses; final List commands = []; - int _responseIndex = 0; @override - Future transceive(String capdu) async { - commands.add(capdu); - return responses[_responseIndex++]; + Future transceive(String command) async { + commands.add(command); + if (responses.isEmpty) { + throw StateError('No queued APDU response for $command'); + } + return responses.removeAt(0); } } diff --git a/test/helper/utils/piv_card_test.dart b/test/helper/utils/piv_card_test.dart index d27f515..fc5fc4d 100644 --- a/test/helper/utils/piv_card_test.dart +++ b/test/helper/utils/piv_card_test.dart @@ -2,6 +2,8 @@ library; import 'dart:io'; +import 'dart:async'; +import 'dart:typed_data'; import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:convert/convert.dart'; @@ -13,6 +15,131 @@ import 'package:flutter_test/flutter_test.dart'; void main() { setUpAll(() => RustLib.init()); tearDownAll(RustLib.dispose); + test('object reads and writes retain the authenticated selection', () async { + final transport = _QueueApduTransport([ + '9000', + '530580038101029000', + '9000', + '6A88', + '53009000', + ]); + await _withPreparedClient(transport, (client) async { + expect(await client.verifyPin('123456'), isTrue); + expect(await client.readObject(0x5fff00), [0x80, 3, 0x81, 1, 2]); + await client.writeObject( + 0x5fff00, + Uint8List.fromList([0x80, 3, 0x81, 1, 3]), + ); + expect(await client.readObject(0x5fc109), isNull); + expect(await client.readObject(0x5fff00), isEmpty); + expect(transport.commands, [ + '0020008008313233343536FFFF', + '00CB3FFF055C035FFF0000', + '00DB3FFF0C5C035FFF0053058003810103', + '00CB3FFF055C035FC10900', + '00CB3FFF055C035FFF0000', + ]); + }); + }); + + test( + 'object writes preserve legacy Le and terminate on uncertain replies', + () async { + for (final response in ['019000', '6C10', '6982']) { + final transport = _QueueApduTransport([response]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.writeObject(0x5fff00, Uint8List(0)), + throwsA(isA()), + ); + expect(transport.commands, ['00DB3FFF075C035FFF00530000']); + await expectLater(client.verifyPin('123456'), throwsStateError); + }, firmware: '1.6.0'); + } + }, + ); + + test('chained object failure never resumes or replays', () async { + final transport = _QueueApduTransport(['9000', '6F00']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.writeObject(0x5fc105, Uint8List(600)), + throwsA(isA()), + ); + expect(transport.commands, hasLength(2)); + expect(transport.commands.first, startsWith('10DB3FFF')); + await expectLater(client.readObject(0x5fff00), throwsStateError); + }); + }); + + test( + 'cancelled object write drains I/O and invalidates preparation', + () async { + PivCardClient? active; + final transport = _QueueApduTransport( + ['9000'], + beforeResponse: (command) async { + if (command.startsWith('10DB')) active!.cancelPendingOperations(); + }, + ); + await _withPreparedClient(transport, (client) async { + active = client; + await expectLater( + client.writeObject(0x5fc105, Uint8List(600)), + throwsStateError, + ); + expect(transport.commands, hasLength(1)); + }); + }, + ); + + test('malformed object does not become absent or keep preparation', () async { + final transport = _QueueApduTransport(['530080009000']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.readObject(0x5fff00), + throwsA( + isA().having( + (e) => e.exchangeAttempted, + 'exchangeAttempted', + isTrue, + ), + ), + ); + await expectLater(client.verifyPin('123456'), throwsStateError); + }); + }); + + test( + 'factory rejection has no exchange evidence and preserves preparation', + () async { + final transport = _QueueApduTransport(['9000']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.verifyPin('1'), + throwsA( + isA().having( + (e) => e.exchangeAttempted, + 'exchangeAttempted', + isFalse, + ), + ), + ); + expect(transport.commands, isEmpty); + expect(await client.verifyPin('123456'), isTrue); + }); + }, + ); + + test('certificate requires an explicitly prepared lease', () async { + final transport = _QueueApduTransport([]); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await expectLater(client.readCertificate(5), throwsStateError); + }); + expect(transport.commands, isEmpty); + }); + test( 'empty VERIFY reads remaining attempts without guessing on success', () async { @@ -22,15 +149,13 @@ void main() { '63C0', '6983', '9000', - '6D00', - '6A88', - '6F00', ]); - final client = PivCardClient(transport: transport); - for (final expected in [3, 1, 0, 0, null, null, null, null]) { - expect(await client.readRemainingPinRetries(), expected); - } - expect(transport.commands, List.filled(8, '0020008000')); + await _withPreparedClient(transport, (client) async { + for (final expected in [3, 1, 0, 0, null]) { + expect(await client.readRemainingPinRetries(), expected); + } + expect(transport.commands, List.filled(5, '0020008000')); + }); }, ); @@ -38,26 +163,182 @@ void main() { 'PUK change and PIN unblock share PIN encoding and report failures', () async { final transport = _QueueApduTransport(['63C2', '9000']); - final client = PivCardClient(transport: transport); + await _withPreparedClient(transport, (client) async { + expect(await client.changePuk('12345678', '87654321'), isFalse); + expect(client.lastStatusWord, '63C2'); + expect(await client.unblockPin('87654321', '123456'), isTrue); + expect(client.lastStatusWord, '9000'); + expect(transport.commands, [ + '002400811031323334353637383837363534333231', + '002C0080103837363534333231313233343536FFFF', + ]); - expect(await client.changePuk('12345678', '87654321'), isFalse); - expect(client.lastStatusWord, '63C2'); - expect(await client.unblockPin('87654321', '123456'), isTrue); - expect(client.lastStatusWord, '9000'); - expect(transport.commands, [ - '002400811031323334353637383837363534333231', - '002C0080103837363534333231313233343536FFFF', - ]); + await expectLater( + client.changePuk('123456789', '87654321'), + throwsA(isA()), + ); + await expectLater( + client.unblockPin('87654321', '123456789'), + throwsA(isA()), + ); + expect(transport.commands, hasLength(2)); + }); + }, + ); - await expectLater( - client.changePuk('123456789', '87654321'), - throwsArgumentError, - ); - await expectLater( - client.unblockPin('87654321', '123456789'), - throwsArgumentError, + test( + 'credential failures keep retries but uncertain writes invalidate preparation', + () async { + final transport = _QueueApduTransport(['63C2', '9000', '019000']); + await _withPreparedClient(transport, (client) async { + expect(await client.verifyPin('123456'), isFalse); + expect(client.lastStatusWord, '63C2'); + expect(await client.verifyPin('123456'), isTrue); + await expectLater( + client.changePin('123456', '654321'), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidResponse', + ), + ), + ); + await expectLater(client.verifyPin('123456'), throwsStateError); + expect(transport.commands, hasLength(3)); + }); + }, + ); + + test( + 'SELECT and session release invalidate prepared credential factories', + () async { + final transport = _QueueApduTransport(['9000']); + await _withPreparedClient(transport, (client) async { + await client.select(); + await expectLater(client.verifyPin('123456'), throwsStateError); + expect(transport.commands, ['00A4040005A00000030800']); + }); + final client = PivCardClient(transport: _QueueApduTransport([])); + await expectLater(client.verifyPin('123456'), throwsStateError); + }, + ); + + test( + 'unexpected status queries stay errors and cannot invent retry counts', + () async { + for (final response in ['6D00', '6A88', '6F00', '019000']) { + await _withPreparedClient(_QueueApduTransport([response]), ( + client, + ) async { + await expectLater( + client.readRemainingPinRetries(), + throwsA(isA()), + ); + await expectLater(client.verifyPin('123456'), throwsStateError); + }); + } + }, + ); + + test( + 'management authentication uses upstream TDES and AES known-answer vectors', + () async { + for (final vector in [ + ( + AlgorithmType.tdes, + '0123456789abcdef23456789abcdef01456789abcdef0123', + '7C0A8108FEDCBA98765432109000', + '0087039B0C7C0A82080737F6C53750D4A400', + ), + ( + AlgorithmType.aes192, + '000102030405060708090a0b0c0d0e0f1011121314151617', + '7C12811000112233445566778899AABBCCDDEEFF9000', + '00870A9B147C128210DDA97CA4864CDFE06EAF70A0EC0D7191', + ), + ]) { + final transport = _QueueApduTransport([ + '9000', + vector.$3, + '9000', + '9000', + ]); + await _withPreparedClient(transport, (client) async { + expect(await client.verifyPin('123456'), isTrue); + expect( + await client.authenticateManagementKey(vector.$2, vector.$1), + isTrue, + ); + expect(await client.readPinRetries(), '9000'); + expect(transport.commands, [ + vector.$1 == AlgorithmType.tdes + ? '0020008008313233343536FFFF00' + : '0020008008313233343536FFFF', + vector.$1 == AlgorithmType.tdes + ? '0087039B047C02810000' + : '00870A9B047C028100', + vector.$4, + '0020008000', + ]); + }, firmware: vector.$1 == AlgorithmType.tdes ? '3.0.3' : '3.1.0'); + } + }, + ); + + test('management authentication owns the lease across both APDUs', () async { + final started = Completer(); + final finish = Completer(); + final transport = _QueueApduTransport( + [ + ..._probeResponses, + '7C12811000112233445566778899AABBCCDDEEFF9000', + '9000', + ], + beforeResponse: (command) async { + if (command == '00870A9B047C028100') { + started.complete(); + await finish.future; + } + }, + ); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + final auth = client.authenticateManagementKey( + '000102030405060708090a0b0c0d0e0f1011121314151617', + AlgorithmType.aes192, ); - expect(transport.commands, hasLength(2)); + await started.future; + await expectLater(client.prepare(), throwsStateError); + await expectLater(client.verifyPin('123456'), throwsStateError); + finish.complete(); + expect(await auth, isTrue); + expect(transport.commands, hasLength(9)); + }); + }); + + test( + 'malformed management challenges are terminal with no authentication response', + () async { + for (final response in [ + '7C038101009000', + '7C0A810800112233445566777C009000', + '6C08', + ]) { + final transport = _QueueApduTransport([response]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.authenticateManagementKey( + '000102030405060708090a0b0c0d0e0f1011121314151617', + AlgorithmType.aes192, + ), + throwsA(isA()), + ); + expect(transport.commands, hasLength(1)); + await expectLater(client.verifyPin('123456'), throwsStateError); + }); + } }, ); @@ -68,9 +349,10 @@ void main() { 'test/fixtures/piv/certificate_response.txt', ).readAsLinesSync(); final transport = _QueueApduTransport(responses); - final certificate = await PivCardClient( - transport: transport, - ).readCertificate(0x05); + final certificate = await _withPreparedClient( + transport, + (client) => client.readCertificate(0x05), + ); expect( certificate, File('test/fixtures/piv/certificate.der').readAsBytesSync(), @@ -78,8 +360,8 @@ void main() { expect(certificate, hasLength(867)); expect(transport.commands, [ '00CB3FFF055C035FC10500', - '00C00000ff', - '00C00000ff', + '00C00000FF', + '00C00000FF', '00C0000072', ]); }, @@ -93,10 +375,11 @@ void main() { '530a7101007003300100fe009000', '530570033001009000', ]) { - final client = PivCardClient( - transport: _QueueApduTransport([response]), - ); - expect(await client.readCertificate(0x0a), [0x30, 0x01, 0x00]); + await _withPreparedClient(_QueueApduTransport([response]), ( + client, + ) async { + expect(await client.readCertificate(0x0a), [0x30, 0x01, 0x00]); + }); } final bytes = [0x30, 0x7e, ...List.filled(126, 0)]; final response = hex.encode([ @@ -115,8 +398,11 @@ void main() { 0x90, 0x00, ]); - final client = PivCardClient(transport: _QueueApduTransport([response])); - expect(await client.readCertificate(0x0a), bytes); + await _withPreparedClient(_QueueApduTransport([response]), ( + client, + ) async { + expect(await client.readCertificate(0x0a), bytes); + }); }, ); @@ -124,9 +410,10 @@ void main() { 'returns null for a missing certificate and rejects invalid objects', () async { for (final sw in ['6A82', '6A88']) { - final missing = PivCardClient(transport: _QueueApduTransport([sw])); - expect(await missing.readCertificate(0x05), isNull); - expect(missing.lastStatusWord, sw); + await _withPreparedClient(_QueueApduTransport([sw]), (missing) async { + expect(await missing.readCertificate(0x05), isNull); + expect(missing.lastStatusWord, sw); + }); } for (final response in [ '70033001009000', // no outer object @@ -135,65 +422,160 @@ void main() { '5382019000', // truncated length '530a700330019000', // truncated value '530970033001007100fe009000', // empty certificate information + '530a700330010070033001009000', // duplicate certificate + '530870033001007101029000', // unsupported information flags + '5305700330010053009000', // duplicate outer object + '5307700330010001009000', // unknown certificate field ]) { - final client = PivCardClient( - transport: _QueueApduTransport([response]), - ); - await expectLater(client.readCertificate(0x05), throwsFormatException); + await _withPreparedClient(_QueueApduTransport([response]), ( + client, + ) async { + await expectLater( + client.readCertificate(0x05), + throwsA(isA()), + ); + }); } - final compressed = PivCardClient( - transport: _QueueApduTransport(['530a7003300100710101fe009000']), + await expectLater( + _withPreparedClient( + _QueueApduTransport(['530a7003300100710101fe009000']), + (client) => client.readCertificate(0x05), + ), + throwsA( + isA().having( + (e) => e.details.kind, + 'kind', + 'InvalidResponse', + ), + ), ); + }, + ); + + test( + 'reads gzip certificates and rejects trailing or corrupt gzip', + () async { + final der = File('test/fixtures/piv/certificate.der').readAsBytesSync(); + final compressed = gzip.encode(der); + List tlv(int tag, List data) => [ + tag, + if (data.length < 128) + data.length + else ...[ + 0x82, + data.length >> 8, + data.length & 0xff, + ], + ...data, + ]; + Future?> read(List payload) { + final object = tlv(0x53, [...tlv(0x70, payload), 0x71, 1, 1, 0xfe, 0]); + final responses = []; + for (var offset = 0; offset < object.length; offset += 256) { + final end = (offset + 256).clamp(0, object.length); + responses.add( + hex.encode([ + ...object.sublist(offset, end), + if (end == object.length) ...[0x90, 0] else ...[0x61, 0], + ]), + ); + } + return _withPreparedClient( + _QueueApduTransport(responses), + (client) => client.readCertificate(0x05), + ); + } + + expect(await read(compressed), der); await expectLater( - compressed.readCertificate(0x05), + read(gzip.encode(List.filled(1024 * 1024 + 1, 0))), throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Compressed'), + isA().having( + (e) => e.details.kind, + 'kind', + 'LimitExceeded', ), ), ); + for (final invalid in [ + [...compressed, 0], + [...compressed, ...compressed], + compressed.sublist(0, compressed.length - 1), + [...compressed]..[compressed.length - 8] ^= 1, + ]) { + await expectLater(read(invalid), throwsA(isA())); + } }, ); + test('certificate errors stay visible and clear stale status', () async { + for (final status in ['6982', '6983', '6D00', '6F00']) { + final transport = _QueueApduTransport([status]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.readCertificate(0x05), + throwsA( + isA().having( + (e) => e.details.statusWord, + 'status', + int.parse(status, radix: 16), + ), + ), + ); + expect(client.lastStatusWord, status); + expect(transport.commands, ['00CB3FFF055C035FC10500']); + }); + } + final transport = _QueueApduTransport([ + '6A88', + 'not hex', + '530570033001009000', + ]); + await _withPreparedClient(transport, (client) async { + expect(await client.readCertificate(0x05), isNull); + await expectLater(client.readCertificate(0x05), throwsFormatException); + expect(client.lastStatusWord, isNull); + expect(await client.readCertificate(0x05), [0x30, 1, 0]); + expect(client.lastStatusWord, '9000'); + for (final id in [-1, 256]) { + await expectLater(client.readCertificate(id), throwsRangeError); + } + expect(transport.commands, hasLength(3)); + }); + }); + test( 'runs PIV metadata and PIN operations through the injected transport', () async { final transport = _QueueApduTransport([ - '9000', '0102039000', - '010203049000', '63C3', '9000', '9000', '9000', '0101FF050101060203039000', ]); - final client = PivCardClient(transport: transport); + await _withPreparedClient(transport, (client) async { + expect(await client.readVersion(), [1, 2, 3]); + expect(await client.readSerial(), '01020304'); + expect(await client.readPinRetries(), '63C3'); + expect(await client.verifyPin('123456'), isTrue); + expect(await client.changePin('123456', '654321'), isTrue); + await client.logout(); + final metadata = await client.readMetadata(0x80); - await client.select(); - expect(await client.readVersion(), [1, 2, 3]); - expect(await client.readSerial(), '01020304'); - expect(await client.readPinRetries(), '63C3'); - expect(await client.verifyPin('123456'), isTrue); - expect(await client.changePin('123456', '654321'), isTrue); - await client.logout(); - final metadata = await client.readMetadata(0x80); - - expect(metadata, isNotNull); - expect(metadata!.retriesCount, 3); - expect(metadata.remainingCount, 3); - expect(transport.commands, [ - '00A4040005A00000030800', - '00FD000000', - '00F8000000', - '0020008000', - '0020008008313233343536FFFF', - '0024008010313233343536FFFF363534333231FFFF', - '0020FF8000', - '00F7008000', - ]); + expect(metadata, isNotNull); + expect(metadata!.retriesCount, 3); + expect(metadata.remainingCount, 3); + expect(transport.commands, [ + '00FD000000', + '0020008000', + '0020008008313233343536FFFF', + '0024008010313233343536FFFF363534333231FFFF', + '0020FF8000', + '00F7008000', + ]); + }); }, ); @@ -201,7 +583,10 @@ void main() { 'blocks PUK through CHANGE REFERENCE DATA until confirmed blocked', () async { final transport = _QueueApduTransport(['63C2', '63C1', '63C0', '6983']); - expect(await PivCardClient(transport: transport).blockPuk(), isTrue); + expect( + await _withPreparedClient(transport, (client) => client.blockPuk()), + isTrue, + ); expect(transport.commands, hasLength(4)); for (final command in transport.commands) { expect(command, startsWith('0024008110')); @@ -219,28 +604,33 @@ void main() { ['9000', '63C0', '6983'], ]) { final transport = _QueueApduTransport(responses); - expect(await PivCardClient(transport: transport).blockPuk(), isTrue); + expect( + await _withPreparedClient(transport, (client) => client.blockPuk()), + isTrue, + ); expect(transport.commands, hasLength(responses.length)); } }, ); - test( - 'does not report PUK blocked on unexpected status or endless retries', - () async { - for (final responses in [ - ['6982'], - ['6A80'], - ['63C0', '6F00'], - List.filled(257, '9000'), - List.filled(257, '63C1'), - ]) { - final transport = _QueueApduTransport(responses); - expect(await PivCardClient(transport: transport).blockPuk(), isFalse); - expect(transport.commands, hasLength(responses.length)); - } - }, - ); + test('PUK blocking stops on unexpected failures without retry', () async { + for (final response in ['6982', '6A80', '6F00', '019000']) { + final transport = _QueueApduTransport([response]); + await expectLater( + _withPreparedClient(transport, (client) => client.blockPuk()), + throwsA(isA()), + ); + expect(transport.commands, hasLength(1)); + } + for (final response in ['9000', '63C1']) { + final transport = _QueueApduTransport(List.filled(257, response)); + expect( + await _withPreparedClient(transport, (client) => client.blockPuk()), + isFalse, + ); + expect(transport.commands, hasLength(257)); + } + }); test( 'libcanokey version handles continuation without an extra SELECT', @@ -283,40 +673,307 @@ void main() { isNull, ); expect( - await PivCardClient( - transport: _QueueApduTransport(['6A88']), - ).readMetadata(0x80), + await _withPreparedClient( + _QueueApduTransport(['6A88']), + (client) => client.readMetadata(0x80), + ), isNull, ); }); test('follows metadata GET RESPONSE and validates PIN length', () async { final transport = _QueueApduTransport(['0101FF6104', '050101060203039000']); + await _withPreparedClient(transport, (client) async { + final metadata = await client.readMetadata(0x80); + + expect(metadata, isNotNull); + expect(transport.commands, ['00F7008000', '00C0000004']); + await expectLater( + client.verifyPin('123456789'), + throwsA(isA()), + ); + }); + }); + + test( + 'metadata never falls back on security, unsupported or malformed responses', + () async { + for (final response in [ + '6982', + '6D00', + '6700', + '0101FF0101FF9000', + '0601039000', + ]) { + final transport = _QueueApduTransport([response]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.readMetadata(0x80), + throwsA(isA()), + ); + expect(transport.commands, ['00F7008000']); + }); + } + }, + ); + + test( + 'metadata profile expires at lease end and reads do not re-probe', + () async { + final transport = _QueueApduTransport([ + ..._probeResponses, + '0101FF050101060203039000', + ]); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await expectLater(client.readMetadata(0x80), throwsStateError); + expect(transport.commands, isEmpty); + await client.prepare(); + expect(await client.readMetadata(0x80), isNotNull); + }); + final count = transport.commands.length; + await client.withSession(() async { + await expectLater(client.readMetadata(0x80), throwsStateError); + }); + expect(transport.commands, hasLength(count)); + }, + ); + + test('metadata follows authentication without a SELECT or probe', () async { + final transport = _QueueApduTransport(['9000', '0101FF050101060203039000']); + await _withPreparedClient(transport, (client) async { + expect(await client.verifyPin('123456'), isTrue); + expect(await client.readMetadata(0x80), isNotNull); + expect(transport.commands, ['0020008008313233343536FFFF', '00F7008000']); + }); + }); + + test( + 'legacy probe omits EE and metadata preserves legacy missing-slot status', + () async { + final responses = [..._probeResponses.take(6)]; + responses[1] = + '322E302E309000'; // Actual firmware 2.0.0, not PIV version. + final transport = _QueueApduTransport([...responses, '6900']); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + expect(transport.commands, hasLength(6)); + expect(await client.readMetadata(0x9a), isNull); + expect(client.lastStatusWord, '6900'); + expect(transport.commands.last, '00F7009A00'); + expect(transport.commands, isNot(contains('00EE010000'))); + }); + }, + ); + + test('incomplete metadata cannot invent UI retry or policy values', () async { + for (final response in ['0101FF9000', '0101FF050102060203039000']) { + await _withPreparedClient(_QueueApduTransport([response]), ( + client, + ) async { + await expectLater(client.readMetadata(0x80), throwsFormatException); + }); + } + }); + + test( + 'competing preparation cannot close an in-flight metadata profile', + () async { + final started = Completer(); + final finish = Completer(); + final transport = _QueueApduTransport( + [..._probeResponses, '010111020200000301019000'], + beforeResponse: (command) async { + if (command == '00F7009A00') { + started.complete(); + await finish.future; + } + }, + ); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + final metadata = client.readMetadata(0x9a); + await started.future; + await expectLater(client.prepare(), throwsStateError); + finish.complete(); + expect((await metadata)!.algorithm, AlgorithmType.eccp256); + expect(transport.commands, hasLength(8)); + }); + }, + ); + + test('failed re-probe cannot leave an old profile available', () async { + final transport = _QueueApduTransport([..._probeResponses, '6F00']); final client = PivCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + await expectLater(client.prepare(), throwsA(isA())); + await expectLater(client.readMetadata(0x80), throwsStateError); + expect(transport.commands, hasLength(8)); + }); + }); - final metadata = await client.readMetadata(0x80); + test( + 'metadata algorithms use this device profile instead of cached UI IDs', + () async { + final responses = [..._probeResponses.take(6)]; + responses[1] = '322E302E309000'; + final transport = _QueueApduTransport([ + ...responses, + '010122020200000301019000', + ]); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + await client.prepare(); + final metadata = await client.readMetadata( + 0x9a, + algorithmExtensionConfig: PivAlgorithmExtensionConfig.defaults, + ); + expect(metadata, isNotNull); + expect(metadata!.algorithm, AlgorithmType.ed25519); + }); + }, + ); - expect(metadata, isNotNull); - expect(transport.commands, ['00F7008000', '00C0000004']); - expect(() => client.verifyPin('123456789'), throwsArgumentError); + test('post-quantum seed import owns INS FE framing and policies', () async { + final transport = _QueueApduTransport(['9000', '9000']); + await _withPreparedClient(transport, (client) async { + await client.importPqSeed( + slot: 0x9a, + kind: 0, + seed: Uint8List.fromList(List.filled(32, 0x07)), + pinPolicy: 2, + touchPolicy: 3, + ); + await client.importPqSeed( + slot: 0x9d, + kind: 1, + seed: Uint8List.fromList(List.filled(64, 0x08)), + ); + expect(client.lastStatusWord, '9000'); + expect(transport.commands, [ + '00FEE29A280920${'07' * 32}AA0102AB0103', + '00FEE39D420A40${'08' * 64}', + ]); + }); }); - test('maps metadata with firmware-specific algorithm extensions', () async { - final metadata = - await PivCardClient( - transport: _QueueApduTransport(['010122050101060203039000']), - ).readMetadata( - 0x80, - algorithmExtensionConfig: PivAlgorithmExtensionConfig.legacyV2, + test( + 'post-quantum seed import rejects invalid arguments before any exchange', + () async { + final transport = _QueueApduTransport(['9000']); + await _withPreparedClient(transport, (client) async { + for (final (slot, kind, length) in [ + (0x9a, 0, 31), // short ML-DSA-65 seed + (0x9a, 1, 32), // ML-KEM-768 seed with the ML-DSA-65 length + (0x9a, 2, 32), // unknown kind + (0x96, 0, 32), // slot outside 9A/9C-9E/82-95 + ]) { + await expectLater( + client.importPqSeed( + slot: slot, + kind: kind, + seed: Uint8List(length), + ), + throwsA( + isA().having( + (e) => e.exchangeAttempted, + 'exchangeAttempted', + isFalse, + ), + ), + ); + } + expect(transport.commands, isEmpty); + await client.importPqSeed(slot: 0x9a, kind: 0, seed: Uint8List(32)); + expect(transport.commands, ['00FEE29A220920${'00' * 32}']); + }); + }, + ); + + test( + 'post-quantum seed import failures retain the status and end preparation', + () async { + final transport = _QueueApduTransport(['6982']); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.importPqSeed(slot: 0x9a, kind: 0, seed: Uint8List(32)), + throwsA(isA()), ); + expect(client.lastStatusWord, '6982'); + expect(transport.commands, ['00FEE29A220920${'00' * 32}']); + await expectLater( + client.importPqSeed(slot: 0x9a, kind: 0, seed: Uint8List(32)), + throwsStateError, + ); + }); + }, + ); + + test( + 'post-quantum seed import requires evidenced PQ algorithm IDs', + () async { + final transport = _QueueApduTransport([]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.importPqSeed(slot: 0x9a, kind: 0, seed: Uint8List(32)), + throwsA( + isA().having( + (e) => e.exchangeAttempted, + 'exchangeAttempted', + isFalse, + ), + ), + ); + expect(transport.commands, isEmpty); + }, firmware: '2.0.0'); + }, + ); +} + +const _probeResponses = [ + '9000', + '332E312E309000', + '43616E6F4B65799000', + '010203049000', + '9000', + '0600009000', + '01E00516E1531554E2E39000', +]; - expect(metadata, isNotNull); - expect(metadata!.algorithm, AlgorithmType.ed25519); +Future _withPreparedClient( + _QueueApduTransport transport, + Future Function(PivCardClient) action, { + String firmware = '3.1.0', +}) async { + final probe = [..._probeResponses]; + final legacy = firmware.startsWith('1.') || firmware.startsWith('2.'); + if (legacy) probe.removeLast(); + probe[1] = '${hex.encode(firmware.codeUnits)}9000'; + transport.responses.insertAll(0, probe); + final client = PivCardClient(transport: transport); + return client.withSession(() async { + await client.prepare(); + expect(transport.commands, [ + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + '00A4040005A00000030800', + '00FD000000', + if (!legacy) '00EE010000', + ]); + transport.commands.clear(); + return action(client); }); } class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); + _QueueApduTransport(List responses, {this.beforeResponse}) + : responses = List.of(responses); + final Future Function(String)? beforeResponse; final List responses; final List commands = []; @@ -325,6 +982,7 @@ class _QueueApduTransport implements ApduTransport { @override Future transceive(String capdu) async { commands.add(capdu); + await beforeResponse?.call(capdu); return responses[_responseIndex++]; } } diff --git a/test/helper/utils/piv_management_key_test.dart b/test/helper/utils/piv_management_key_test.dart deleted file mode 100644 index 142a7a2..0000000 --- a/test/helper/utils/piv_management_key_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:canokey_console/helper/utils/piv_management_key.dart'; -import 'package:canokey_console/models/piv.dart'; -import 'package:convert/convert.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('PivManagementKeyProtocol', () { - test('builds legacy 3DES authentication APDUs', () { - expect( - PivManagementKeyProtocol.authenticateRequest(AlgorithmType.tdes), - '0087039B047C028100', - ); - expect( - PivManagementKeyProtocol.authenticateResponse( - AlgorithmType.tdes, - hex.decode('0011223344556677'), - ), - '0087039B0C7C0A82080011223344556677', - ); - }); - - test('builds AES-192 authentication APDUs', () { - expect( - PivManagementKeyProtocol.authenticateRequest(AlgorithmType.aes192), - '00870A9B047C028100', - ); - expect( - PivManagementKeyProtocol.authenticateResponse( - AlgorithmType.aes192, - hex.decode('00112233445566778899AABBCCDDEEFF'), - ), - '00870A9B147C12821000112233445566778899AABBCCDDEEFF', - ); - }); - - test('parses 3DES and AES-192 challenges', () { - expect( - hex.encode(PivManagementKeyProtocol.parseChallenge( - hex.decode('7C0A81080011223344556677'), - AlgorithmType.tdes, - )), - '0011223344556677', - ); - expect( - hex.encode(PivManagementKeyProtocol.parseChallenge( - hex.decode('7C12811000112233445566778899AABBCCDDEEFF'), - AlgorithmType.aes192, - )), - '00112233445566778899aabbccddeeff', - ); - }); - - test('rejects a challenge with the wrong block length', () { - expect( - () => PivManagementKeyProtocol.parseChallenge( - hex.decode('7C0A81080011223344556677'), - AlgorithmType.aes192, - ), - throwsFormatException, - ); - }); - - test('rejects an invalid management key before calling Rust', () { - expect( - () => PivManagementKeyProtocol.encryptChallenge( - algorithm: AlgorithmType.aes192, - key: const [0x00], - challenge: hex.decode('00112233445566778899AABBCCDDEEFF'), - ), - throwsArgumentError, - ); - }); - - test('rejects an invalid challenge before calling Rust', () { - expect( - () => PivManagementKeyProtocol.encryptChallenge( - algorithm: AlgorithmType.tdes, - key: List.filled(24, 0), - challenge: const [0x00], - ), - throwsArgumentError, - ); - }); - - test('builds SET MANAGEMENT KEY APDUs for legacy and new core', () { - const key = '010203040506070801020304050607080102030405060708'; - expect( - PivManagementKeyProtocol.setManagementKey( - algorithm: AlgorithmType.tdes, - key: key, - touchPolicy: TouchPolicy.never, - ), - '00FFFFFF1B039B18$key', - ); - expect( - PivManagementKeyProtocol.setManagementKey( - algorithm: AlgorithmType.aes192, - key: key, - touchPolicy: TouchPolicy.never, - ), - '00FFFFFF1B0A9B18$key', - ); - expect( - PivManagementKeyProtocol.setManagementKey( - algorithm: AlgorithmType.aes192, - key: key, - touchPolicy: TouchPolicy.always, - ), - '00FFFFFE1B0A9B18$key', - ); - }); - }); -} diff --git a/test/helper/utils/piv_post_quantum_test.dart b/test/helper/utils/piv_post_quantum_test.dart deleted file mode 100644 index e19a260..0000000 --- a/test/helper/utils/piv_post_quantum_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:typed_data'; - -import 'package:canokey_console/helper/utils/piv_post_quantum.dart'; -import 'package:canokey_console/models/piv.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('builds the ML-DSA-65 seed-only import template', () { - final seed = Uint8List.fromList(List.generate(32, (index) => index)); - - final data = PivPostQuantumProtocol.buildImportData( - algorithm: AlgorithmType.mldsa65, - seed: seed, - pinPolicy: PinPolicy.once, - touchPolicy: TouchPolicy.cached, - ); - - expect(data.sublist(0, 2), [0x09, 0x20]); - expect(data.sublist(2, 34), seed); - expect(data.sublist(34), [0xAA, 0x01, 0x02, 0xAB, 0x01, 0x03]); - }); - - test('builds the ML-KEM-768 d || z import template', () { - final seed = - Uint8List.fromList(List.generate(64, (index) => 0x40 + index)); - - final data = PivPostQuantumProtocol.buildImportData( - algorithm: AlgorithmType.mlkem768, - seed: seed, - pinPolicy: PinPolicy.never, - touchPolicy: TouchPolicy.always, - ); - - expect(data.sublist(0, 2), [0x0A, 0x40]); - expect(data.sublist(2, 66), seed); - expect(data.sublist(66), [0xAA, 0x01, 0x01, 0xAB, 0x01, 0x02]); - }); - - test('builds the chained ML-KEM-768 decapsulation payload', () { - final ciphertext = Uint8List.fromList(List.generate( - PivPostQuantumProtocol.mlKem768CiphertextLength, - (index) => index & 0xFF)); - - final data = PivPostQuantumProtocol.buildMlKemDecapsulationData(ciphertext); - - expect(data.length, 1098); - expect(data.sublist(0, 10), - [0x7C, 0x82, 0x04, 0x46, 0x82, 0x00, 0x81, 0x82, 0x04, 0x40]); - expect(data.sublist(10), ciphertext); - }); - - test('parses the wrapped ML-KEM-768 shared secret', () { - final secret = List.generate(32, (index) => 0xA0 + index); - - final parsed = PivPostQuantumProtocol.parseMlKemSharedSecret( - [0x7C, 0x22, 0x82, 0x20, ...secret]); - - expect(parsed, secret); - }); - - test('rejects invalid post-quantum material lengths', () { - expect( - () => PivPostQuantumProtocol.buildImportData( - algorithm: AlgorithmType.mldsa65, - seed: Uint8List(31), - pinPolicy: PinPolicy.once, - touchPolicy: TouchPolicy.never, - ), - throwsArgumentError, - ); - expect( - () => PivPostQuantumProtocol.buildMlKemDecapsulationData(Uint8List(1087)), - throwsArgumentError, - ); - expect( - () => PivPostQuantumProtocol.parseMlKemSharedSecret( - [0x7C, 0x03, 0x82, 0x01, 0x00]), - throwsArgumentError, - ); - }); -} diff --git a/test/helper/utils/piv_signature_test.dart b/test/helper/utils/piv_signature_test.dart deleted file mode 100644 index 9fdfb4a..0000000 --- a/test/helper/utils/piv_signature_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:canokey_console/helper/utils/piv_signature.dart'; -import 'package:canokey_console/models/piv.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('uses the configured Ed25519 ID through the classic size limit', () { - expect( - PivSignatureProtocol.generalAuthenticateAlgorithmId( - algorithm: AlgorithmType.ed25519, - messageLength: PivSignatureProtocol.classicEd25519MaxMessageLength, - config: PivAlgorithmExtensionConfig.legacyV2, - ), - PivAlgorithmExtensionConfig.legacyV2.ed25519, - ); - }); - - test('uses randomized Ed25519 mode for messages over the classic limit', () { - expect( - PivSignatureProtocol.generalAuthenticateAlgorithmId( - algorithm: AlgorithmType.ed25519, - messageLength: PivSignatureProtocol.classicEd25519MaxMessageLength + 1, - config: PivAlgorithmExtensionConfig.legacyV2, - ), - PivSignatureProtocol.randomizedEd25519AlgorithmId, - ); - }); - - test('requires algorithm extensions for randomized Ed25519 mode', () { - final disabled = PivAlgorithmExtensionConfig( - enabled: false, - ed25519: PivAlgorithmExtensionConfig.defaults.ed25519, - rsa3072: PivAlgorithmExtensionConfig.defaults.rsa3072, - rsa4096: PivAlgorithmExtensionConfig.defaults.rsa4096, - x25519: PivAlgorithmExtensionConfig.defaults.x25519, - secp256k1: PivAlgorithmExtensionConfig.defaults.secp256k1, - sm2: PivAlgorithmExtensionConfig.defaults.sm2, - secp521r1: PivAlgorithmExtensionConfig.defaults.secp521r1, - mldsa65: PivAlgorithmExtensionConfig.defaults.mldsa65, - mlkem768: PivAlgorithmExtensionConfig.defaults.mlkem768, - ); - - expect( - () => PivSignatureProtocol.generalAuthenticateAlgorithmId( - algorithm: AlgorithmType.ed25519, - messageLength: PivSignatureProtocol.classicEd25519MaxMessageLength + 1, - config: disabled, - ), - throwsUnsupportedError, - ); - }); - - test('keeps configured IDs for algorithms without a split signing mode', () { - expect( - PivSignatureProtocol.generalAuthenticateAlgorithmId( - algorithm: AlgorithmType.mldsa65, - messageLength: 4096, - config: PivAlgorithmExtensionConfig.defaults, - ), - PivAlgorithmExtensionConfig.defaults.mldsa65, - ); - }); -} diff --git a/test/helper/utils/protocol_operation_test.dart b/test/helper/utils/protocol_operation_test.dart index b1cbd30..26937be 100644 --- a/test/helper/utils/protocol_operation_test.dart +++ b/test/helper/utils/protocol_operation_test.dart @@ -1,4 +1,6 @@ import 'dart:typed_data'; +import 'dart:async'; +import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; @@ -92,6 +94,119 @@ void main() { expect(operation.closed && operation.disposed, isTrue); }); + test( + 'cancellation waits for I/O and closes before releasing the lease', + () async { + final sessions = CardSessions(); + final cancellation = CardCancellation(); + final response = Completer(); + final started = Completer(); + final command = Uint8List.fromList([0, 0xfd, 0, 0, 0]); + final operation = _Operation([ProtocolStep(command: command)]); + final useCase = sessions.run((session) async { + session.bind((_) { + started.complete(); + return response.future; + }); + await expectLater( + executeProtocolOperation( + operation, + _Transport((_) async => throw StateError('Wrong transport')), + lease: session.lease, + cancellation: cancellation, + ), + throwsStateError, + ); + }); + await started.future; + cancellation.cancel(); + var nextStarted = false; + final next = sessions.run((_) async { + expect(operation.closed && operation.disposed, isTrue); + nextStarted = true; + }); + await Future.delayed(Duration.zero); + expect(operation.closed, isFalse); + expect(nextStarted, isFalse); + response.complete('0600009000'); + await Future.wait([useCase, next]); + expect(operation.responses, isEmpty); + expect(command, everyElement(0)); + }, + ); + + test('connection replacement rejects response before Rust advance', () async { + final sessions = CardSessions(); + final operation = _Operation([ProtocolStep(command: Uint8List(5))]); + await sessions.run((session) async { + session.bind((_) async { + sessions.invalidate(); + return '9000'; + }); + await expectLater( + executeProtocolOperation( + operation, + _Transport((_) async => throw StateError('Wrong transport')), + lease: session.lease, + ), + throwsStateError, + ); + }); + expect(operation.responses, isEmpty); + expect(operation.closed && operation.disposed, isTrue); + }); + + test('cancellation before start performs no exchange and closes', () async { + final cancellation = CardCancellation()..cancel(); + final operation = _Operation([]); + await expectLater( + executeProtocolOperation( + operation, + _Transport((_) async => fail('Unexpected exchange')), + cancellation: cancellation, + ), + throwsStateError, + ); + expect(operation.closed && operation.disposed, isTrue); + }); + + test( + 'production transport refuses an operation outside a use-case lease', + () async { + final operation = _Operation([]); + await expectLater( + executeProtocolOperation(operation, const SmartCardApduTransport()), + throwsStateError, + ); + expect(operation.closed && operation.disposed, isTrue); + }, + ); + + test('cancelled probe result is freed instead of published', () async { + final cancellation = CardCancellation(); + final profile = _Profile(); + final operation = _Operation([ + ProtocolStep(profile: profile), + ], onStart: cancellation.cancel); + final sessions = CardSessions(); + await sessions.run((session) async { + session.bind((_) async => fail('Unexpected exchange')); + await expectLater( + executeProfileProbe( + operation, + _Transport((_) async => fail('Unexpected exchange')), + lease: session.lease, + cancellation: cancellation, + ), + throwsStateError, + ); + expect(profile.closed && profile.disposed, isTrue); + // The executor releases its reservation even when result publication fails. + session.lease.beginOperation().close(); + }); + expect(operation.closed && operation.disposed, isTrue); + }); + test('unexpected bridge exception still releases the operation', () async { final operation = _Operation([]); await expectLater( @@ -103,7 +218,8 @@ void main() { } class _Operation implements ProtocolOperation { - _Operation(this.steps); + _Operation(this.steps, {this.onStart}); + final void Function()? onStart; final List steps; final responses = >[]; Uint8List? borrowedResponse; @@ -111,7 +227,11 @@ class _Operation implements ProtocolOperation { bool disposed = false; @override - ProtocolStep start() => steps.removeAt(0); + ProtocolStep start() { + onStart?.call(); + return steps.removeAt(0); + } + @override ProtocolStep advance({required List response}) { responses.add(List.of(response)); @@ -133,3 +253,14 @@ class _Transport implements ApduTransport { @override Future transceive(String capdu) => exchange(capdu); } + +class _Profile implements ProtocolProfile { + bool closed = false; + bool disposed = false; + @override + void close() => closed = true; + @override + void dispose() => disposed = true; + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index d198880..814da71 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -11,6 +11,7 @@ import 'package:canokey_console/helper/utils/oath_card.dart'; import 'package:canokey_console/helper/utils/openpgp_card.dart'; import 'package:canokey_console/helper/utils/pass_card.dart'; import 'package:canokey_console/helper/utils/piv_card.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/oath.dart'; import 'package:canokey_console/models/openpgp.dart'; @@ -48,24 +49,12 @@ class ApduResponse { class ConsoleSmoke { ConsoleSmoke({required this.card, required this.expectedVersion}) { - _adminClient = AdminCardClient( - transport: _clientTransport('admin'), - ); - _openPgpClient = OpenPgpCardClient( - transport: _clientTransport('openpgp'), - ); - _ndefClient = NdefCardClient( - transport: _clientTransport('ndef'), - ); - _oathClient = OathCardClient( - transport: _clientTransport('oath'), - ); - _passClient = PassCardClient( - transport: _clientTransport('pass'), - ); - _pivClient = PivCardClient( - transport: _clientTransport('piv'), - ); + _adminClient = AdminCardClient(transport: _clientTransport('admin')); + _openPgpClient = OpenPgpCardClient(transport: _clientTransport('openpgp')); + _ndefClient = NdefCardClient(transport: _clientTransport('ndef')); + _oathClient = OathCardClient(transport: _clientTransport('oath')); + _passClient = PassCardClient(transport: _clientTransport('pass')); + _pivClient = PivCardClient(transport: _clientTransport('piv')); _webAuthnTransmitter = CtapTransmitter( transport: _clientTransport('webauthn'), ); @@ -163,132 +152,175 @@ class ConsoleSmoke { } Future _adminApplet() async { - await _adminClient.select(); - final actualVersion = await _adminClient.readFirmwareVersion(); - _expect( - actualVersion == expectedVersion, - 'Expected firmware $expectedVersion, got $actualVersion', - ); - - _expect((await _adminClient.readModel()).isNotEmpty, 'Empty model'); - - _adminSerial = await _adminClient.readSerial(); - _expect(_adminSerial.length == 8, 'Serial number must contain four bytes'); - - _expect((await _adminClient.readChipId()).isNotEmpty, 'Empty chip ID'); - - _expect( - await _adminClient.verifyPin(_defaultAdminPin), - 'Console failed to verify the default admin PIN', - ); - _functionSet = CanoKey.functionSetFromFirmwareVersion(actualVersion); - final minimumConfigBytes = switch (_functionSet) { - FunctionSetVersion.v1 => 7, - FunctionSetVersion.v2 => 5, - FunctionSetVersion.v3 => 6, - FunctionSetVersion.v4 => 5, - FunctionSetVersion.v5 => 6, - }; - final configBytes = await _adminClient.readConfig(); - _expect( - configBytes.length >= minimumConfigBytes, - 'Config for ${_functionSet.name} is shorter than $minimumConfigBytes bytes', - ); - final booleanIndexes = switch (_functionSet) { - FunctionSetVersion.v1 => const [0, 1, 2, 3, 4, 5], - FunctionSetVersion.v2 => const [0, 1, 2, 3, 4], - FunctionSetVersion.v3 => const [0, 1, 2, 3, 4, 5], - FunctionSetVersion.v4 => const [0, 2, 3, 4], - FunctionSetVersion.v5 => const [0, 2, 3, 4], - }; - for (final index in booleanIndexes) { + await _adminClient.withSession(() async { + await _adminClient.prepare(); + final actualVersion = await _adminClient.readFirmwareVersion(); _expect( - configBytes[index] == 0 || configBytes[index] == 1, - 'Admin config byte $index is not boolean: ${configBytes[index]}', + actualVersion == expectedVersion, + 'Expected firmware $expectedVersion, got $actualVersion', ); - } - _initialNdefReadonly = configBytes[2] == 1; - final initialLed = configBytes[0]; - final toggledLed = initialLed == 0 ? 1 : 0; - await _adminClient.writeConfigByte(1, toggledLed); - var changedConfig = await _adminClient.readConfig(); - _expect( - changedConfig[0] == toggledLed, - 'LED configuration did not change', - ); - await _adminClient.writeConfigByte(1, initialLed); - changedConfig = await _adminClient.readConfig(); - _expect( - changedConfig[0] == initialLed, - 'LED configuration was not restored', - ); + _expect((await _adminClient.readModel()).isNotEmpty, 'Empty model'); - if (_functionSet.index >= FunctionSetVersion.v4.index) { - final initialNfc = await _adminClient.readNfcEnabled(); - await _adminClient.setNfcEnabled(!initialNfc); + _adminSerial = await _adminClient.readSerial(); _expect( - await _adminClient.readNfcEnabled() == !initialNfc, - 'NFC state did not change', + _adminSerial.length == 8, + 'Serial number must contain four bytes', ); - await _adminClient.setNfcEnabled(initialNfc); + + _expect((await _adminClient.readChipId()).isNotEmpty, 'Empty chip ID'); + _expect( - await _adminClient.readNfcEnabled() == initialNfc, - 'NFC state was not restored', + await _adminClient.verifyPin(_defaultAdminPin), + 'Console failed to verify the default admin PIN', ); - } - if (_functionSet == FunctionSetVersion.v5) { - await _sm2Configuration(); - final storage = await _adminClient.readStorageUsage(); + _functionSet = CanoKey.functionSetFromFirmwareVersion(actualVersion); + final minimumConfigBytes = switch (_functionSet) { + FunctionSetVersion.v1 => 7, + FunctionSetVersion.v2 => 5, + FunctionSetVersion.v3 => 6, + FunctionSetVersion.v4 => 5, + FunctionSetVersion.v5 => 6, + }; + final configBytes = await _adminClient.readConfig(pin: _defaultAdminPin); _expect( - storage.usedKiB <= storage.totalKiB, - 'Used storage exceeds total storage', + configBytes.length >= minimumConfigBytes, + 'Config for ${_functionSet.name} is shorter than $minimumConfigBytes bytes', ); + final booleanIndexes = switch (_functionSet) { + FunctionSetVersion.v1 => const [0, 1, 2, 3, 4, 5], + FunctionSetVersion.v2 => const [0, 1, 2, 3, 4], + FunctionSetVersion.v3 => const [0, 1, 2, 3, 4, 5], + FunctionSetVersion.v4 => const [0, 2, 3, 4], + FunctionSetVersion.v5 => const [0, 2, 3, 4], + }; + for (final index in booleanIndexes) { + _expect( + configBytes[index] == 0 || configBytes[index] == 1, + 'Admin config byte $index is not boolean: ${configBytes[index]}', + ); + } + _initialNdefReadonly = configBytes[2] == 1; - final featureMask = configBytes[5]; - final toggledMask = featureMask ^ 0x01; - await _adminClient.writeConfigByte(6, toggledMask); - changedConfig = await _adminClient.readConfig(); + final initialLed = configBytes[0]; + final toggledLed = initialLed == 0 ? 1 : 0; + await _adminClient.configure( + pin: _defaultAdminPin, + ledOn: toggledLed == 1, + ); + await _adminClient.prepare(); + var changedConfig = await _adminClient.readConfig(pin: _defaultAdminPin); _expect( - changedConfig[5] == toggledMask, - 'Pass feature switch did not change', + changedConfig[0] == toggledLed, + 'LED configuration did not change', + ); + await _adminClient.configure( + pin: _defaultAdminPin, + ledOn: initialLed == 1, ); - await _adminClient.writeConfigByte(6, featureMask); - changedConfig = await _adminClient.readConfig(); + await _adminClient.prepare(); + changedConfig = await _adminClient.readConfig(pin: _defaultAdminPin); _expect( - changedConfig[5] == featureMask, - 'Pass feature switch was not restored', + changedConfig[0] == initialLed, + 'LED configuration was not restored', ); - } - stdout.writeln('ok: admin.client.read_and_restore_config'); + + if (_functionSet.index >= FunctionSetVersion.v4.index) { + final initialNfc = await _adminClient.readNfcEnabled( + pin: _defaultAdminPin, + ); + await _adminClient.setNfcEnabled(!initialNfc, pin: _defaultAdminPin); + await _adminClient.prepare(); + _expect( + await _adminClient.readNfcEnabled(pin: _defaultAdminPin) == + !initialNfc, + 'NFC state did not change', + ); + await _adminClient.setNfcEnabled(initialNfc, pin: _defaultAdminPin); + await _adminClient.prepare(); + _expect( + await _adminClient.readNfcEnabled(pin: _defaultAdminPin) == + initialNfc, + 'NFC state was not restored', + ); + } + if (_functionSet == FunctionSetVersion.v5) { + await _sm2Configuration(); + final storage = await _adminClient.readStorageUsage( + pin: _defaultAdminPin, + ); + _expect( + storage.usedKiB <= storage.totalKiB, + 'Used storage exceeds total storage', + ); + + final featureMask = configBytes[5]; + final toggledMask = featureMask ^ 0x01; + await _adminClient.configure( + pin: _defaultAdminPin, + featureMask: 0x3f, + featureValues: toggledMask, + ); + await _adminClient.prepare(); + changedConfig = await _adminClient.readConfig(pin: _defaultAdminPin); + _expect( + changedConfig[5] == toggledMask, + 'Pass feature switch did not change', + ); + await _adminClient.configure( + pin: _defaultAdminPin, + featureMask: 0x3f, + featureValues: featureMask, + ); + await _adminClient.prepare(); + changedConfig = await _adminClient.readConfig(pin: _defaultAdminPin); + _expect( + changedConfig[5] == featureMask, + 'Pass feature switch was not restored', + ); + } + stdout.writeln('ok: admin.client.read_and_restore_config'); + }); } Future _sm2Configuration() async { - final original = await _adminClient.readSm2Config(); + final original = await _adminClient.readSm2Config(pin: _defaultAdminPin); _expect( - !original.canChangeEnabled, 'Current SM2 config must use eight bytes'); + !original.canChangeEnabled, + 'Current SM2 config must use eight bytes', + ); _expect(original.enabled, 'Current firmware always enables SM2'); try { await _adminClient.writeSm2Config( + pin: _defaultAdminPin, enabled: true, curveId: 0x1234567, algoId: -0x1234568, ); - final changed = await _adminClient.readSm2Config(); - _expect(changed.curveId == 0x1234567 && changed.algoId == -0x1234568, - 'SM2 identifiers did not roundtrip in big-endian order'); + await _adminClient.prepare(); + final changed = await _adminClient.readSm2Config(pin: _defaultAdminPin); + _expect( + changed.curveId == 0x1234567 && changed.algoId == -0x1234568, + 'SM2 identifiers did not roundtrip in big-endian order', + ); } finally { + // A new explicit cleanup operation must rediscover after the prior write, + // including failure; it does not resume the failed operation. + await _adminClient.prepare(); await _adminClient.writeSm2Config( + pin: _defaultAdminPin, enabled: original.enabled, curveId: original.curveId, algoId: original.algoId, ); } - final restored = await _adminClient.readSm2Config(); + await _adminClient.prepare(); + final restored = await _adminClient.readSm2Config(pin: _defaultAdminPin); _expect( - restored.curveId == original.curveId && - restored.algoId == original.algoId, - 'SM2 configuration was not restored'); + restored.curveId == original.curveId && + restored.algoId == original.algoId, + 'SM2 configuration was not restored', + ); stdout.writeln('ok: admin.client.sm2_config'); } @@ -341,61 +373,64 @@ class ConsoleSmoke { _expect(challenge.data.length == 16, 'OpenPGP challenge must be 8 bytes'); } - final cardInfo = await _openPgpClient.readCardInfo(); - _expect( - cardInfo.version.isNotEmpty, - 'Console parsed an empty OpenPGP version', - ); - _expect( - cardInfo.serialNumber.length == 8, - 'Console parsed an invalid OpenPGP serial number', - ); - _expect( - cardInfo.pinState.userRetries != null && - cardInfo.pinState.adminRetries != null, - 'Console did not parse OpenPGP PIN retries', - ); - _expect( - cardInfo.keySlots.length == OpenPgpKeyType.values.length, - 'Console did not parse every OpenPGP key slot', - ); - stdout.writeln('ok: openpgp.client.read_card_info'); + await _openPgpClient.withSession(() async { + await _openPgpClient.prepare(); + final cardInfo = await _openPgpClient.readCardInfo(); + _expect( + cardInfo.version.isNotEmpty, + 'Console parsed an empty OpenPGP version', + ); + _expect( + cardInfo.serialNumber.length == 8, + 'Console parsed an invalid OpenPGP serial number', + ); + _expect( + cardInfo.pinState.userRetries != null && + cardInfo.pinState.adminRetries != null, + 'Console did not parse OpenPGP PIN retries', + ); + _expect( + cardInfo.keySlots.length == OpenPgpKeyType.values.length, + 'Console did not parse every OpenPGP key slot', + ); + stdout.writeln('ok: openpgp.client.read_card_info'); - _expect( - await _openPgpClient.changeUserPin( - _defaultOpenPgpUserPin, - _alternateOpenPgpUserPin, - ), - 'Console failed to change the OpenPGP user PIN', - ); - _expect( - await _openPgpClient.changeUserPin( - _alternateOpenPgpUserPin, - _defaultOpenPgpUserPin, - ), - 'Console failed to restore the OpenPGP user PIN', - ); - _expect( - await _openPgpClient.changeAdminPin( - _defaultOpenPgpAdminPin, - _alternateOpenPgpAdminPin, - ), - 'Console failed to change the OpenPGP admin PIN', - ); - _expect( - await _openPgpClient.changeAdminPin( - _alternateOpenPgpAdminPin, - _defaultOpenPgpAdminPin, - ), - 'Console failed to restore the OpenPGP admin PIN', - ); - stdout.writeln('ok: openpgp.client.change_and_restore_pins'); + _expect( + await _openPgpClient.changeUserPin( + _defaultOpenPgpUserPin, + _alternateOpenPgpUserPin, + ), + 'Console failed to change the OpenPGP user PIN', + ); + _expect( + await _openPgpClient.changeUserPin( + _alternateOpenPgpUserPin, + _defaultOpenPgpUserPin, + ), + 'Console failed to restore the OpenPGP user PIN', + ); + _expect( + await _openPgpClient.changeAdminPin( + _defaultOpenPgpAdminPin, + _alternateOpenPgpAdminPin, + ), + 'Console failed to change the OpenPGP admin PIN', + ); + _expect( + await _openPgpClient.changeAdminPin( + _alternateOpenPgpAdminPin, + _defaultOpenPgpAdminPin, + ), + 'Console failed to restore the OpenPGP admin PIN', + ); + stdout.writeln('ok: openpgp.client.change_and_restore_pins'); - await _openPgpPinRecovery(); - await _openPgpPolicies(cardInfo); - if (_functionSet == FunctionSetVersion.v5) { - await _openPgpPinRetries(cardInfo.pinState); - } + await _openPgpPinRecovery(); + await _openPgpPolicies(cardInfo); + if (_functionSet == FunctionSetVersion.v5) { + await _openPgpPinRetries(cardInfo.pinState); + } + }); } Future _openPgpPinRecovery() async { @@ -609,8 +644,9 @@ class ConsoleSmoke { final originalUserRetries = originalState.userRetries!; final originalResetRetries = originalState.resetRetries!; final originalAdminRetries = originalState.adminRetries!; - final restoreResetRetries = - originalResetRetries == 0 ? 3 : originalResetRetries; + final restoreResetRetries = originalResetRetries == 0 + ? 3 + : originalResetRetries; var temporaryResetCodeConfigured = false; var retriesChanged = false; try { @@ -672,153 +708,139 @@ class ConsoleSmoke { } Future _pivApplet() async { - await _pivClient.select(); - _expect( - (await _pivClient.readVersion()).length == 3, - 'PIV version must contain three bytes', - ); - _expect( - await _pivClient.readSerial() == _adminSerial.toUpperCase(), - 'PIV and Admin serial numbers differ', - ); + await _pivClient.withSession(() async { + await _pivClient.prepare(); + _expect( + (await _pivClient.readVersion()).length == 3, + 'PIV version must contain three bytes', + ); + _expect( + await _pivClient.readSerial() == _adminSerial.toUpperCase(), + 'Probed serial differs from the initial Admin serial', + ); - var response = ApduResponse.parse(await _pivClient.readPinRetries()); - _expect( - response.statusWord == '63C3', - 'Console parsed unexpected PIV PIN retries', - ); - _expect( - await _pivClient.verifyPin(_defaultPivPin), - 'Console failed to verify the default PIV PIN', - ); - response = ApduResponse.parse(await _pivClient.readPinRetries()); - _expect(response.statusWord == '9000', - 'Console did not retain PIV PIN verification'); + var response = ApduResponse.parse(await _pivClient.readPinRetries()); + _expect( + response.statusWord == '63C3', + 'Console parsed unexpected PIV PIN retries', + ); + _expect( + await _pivClient.verifyPin(_defaultPivPin), + 'Console failed to verify the default PIV PIN', + ); + response = ApduResponse.parse(await _pivClient.readPinRetries()); + _expect( + response.statusWord == '9000', + 'Console did not retain PIV PIN verification', + ); - _expect( - await _pivClient.changePin(_defaultPivPin, _alternatePivPin), - 'Console failed to change the PIV PIN', - ); - _expect( - await _pivClient.verifyPin(_alternatePivPin), - 'Console failed to verify the alternate PIV PIN', - ); - _expect( - await _pivClient.changePin(_alternatePivPin, _defaultPivPin), - 'Console failed to restore the PIV PIN', - ); - await _pivClient.logout(); - response = ApduResponse.parse(await _pivClient.readPinRetries()); - _expect( - response.statusWord == '63C3', - 'Console did not observe PIV logout', - ); + _expect( + await _pivClient.changePin(_defaultPivPin, _alternatePivPin), + 'Console failed to change the PIV PIN', + ); + _expect( + await _pivClient.verifyPin(_alternatePivPin), + 'Console failed to verify the alternate PIV PIN', + ); + _expect( + await _pivClient.changePin(_alternatePivPin, _defaultPivPin), + 'Console failed to restore the PIV PIN', + ); + await _pivClient.logout(); + response = ApduResponse.parse(await _pivClient.readPinRetries()); + _expect( + response.statusWord == '63C3', + 'Console did not observe PIV logout', + ); - final firmware = FirmwareVersion.parse(expectedVersion); - if (firmware.compareTo(const FirmwareVersion(2, 0, 0)) >= 0) { - for (final slot in const [0x80, 0x81, 0x9B]) { - final metadata = await _pivClient.readMetadata(slot); - _expect( - metadata != null, - 'Console could not parse PIV metadata for $slot', - ); - final info = metadata!; - _expect(info.number == slot, 'PIV metadata returned the wrong slot'); - if (slot == 0x80 || slot == 0x81) { - _expect(info.retriesCount > 0, 'PIV slot $slot has no retry limit'); + final firmware = FirmwareVersion.parse(expectedVersion); + if (firmware.compareTo(const FirmwareVersion(2, 0, 0)) >= 0) { + for (final slot in const [0x80, 0x81, 0x9B]) { + final metadata = await _pivClient.readMetadata(slot); _expect( - info.remainingCount <= info.retriesCount, - 'PIV slot $slot remaining retries exceed its limit', + metadata != null, + 'Console could not parse PIV metadata for $slot', ); + final info = metadata!; + _expect(info.number == slot, 'PIV metadata returned the wrong slot'); + if (slot == 0x80 || slot == 0x81) { + _expect(info.retriesCount > 0, 'PIV slot $slot has no retry limit'); + _expect( + info.remainingCount <= info.retriesCount, + 'PIV slot $slot remaining retries exceed its limit', + ); + } } } - } - await _pivClient.readAlgorithmExtensions(); - stdout.writeln('ok: piv.client.read_change_and_restore'); + await _pivClient.readAlgorithmExtensions(); + stdout.writeln('ok: piv.client.read_change_and_restore'); + }); } Future _oathApplet() async { - final selection = await _oathClient.select(); - if (selection.version != OathVersion.legacy) { - _expect(selection.info.containsKey(0x79), 'OATH select has no version'); - _expect(selection.info.containsKey(0x71), 'OATH select has no device ID'); - } + await _oathClient.withSession(() async { + await _oathClient.prepare(); + final selection = await _oathClient.select(); + if (selection.version != OathVersion.legacy) { + _expect( + selection.salt != null && selection.salt!.length == 8, + 'OATH select has no device ID', + ); + } - const account = 'Console:USBIP'; - final secret = _encodeHex( - List.generate(20, (index) => index + 1), - ); - var response = ApduResponse.parse( + const account = 'Console:USBIP'; + final secret = _encodeHex(List.generate(20, (index) => index + 1)); await _oathClient.put( name: account, secretHex: secret, type: OathType.totp, algorithm: OathAlgorithm.sha1, digits: 6, - ), - ); - _expect( - response.statusWord == '9000', 'Console failed to add OATH account'); - response = ApduResponse.parse( - await _oathClient.put( - name: account, - secretHex: secret, - type: OathType.totp, - algorithm: OathAlgorithm.sha1, - digits: 6, - ), - ); - _expect( - response.statusWord == '6985', - 'Console did not detect a duplicate OATH account', - ); + ); + + var duplicateDetected = false; + try { + await _oathClient.put( + name: account, + secretHex: secret, + type: OathType.totp, + algorithm: OathAlgorithm.sha1, + digits: 6, + ); + } on ProtocolException catch (e) { + duplicateDetected = e.details.statusWord == 0x6985; + } + _expect( + duplicateDetected, + 'Console did not detect a duplicate OATH account', + ); - const challenge = '0000000000000001'; - final calculation = ApduResponse.parse( - await _oathClient.calculate( + const challenge = '0000000000000001'; + final (digits, _) = await _oathClient.calculate( name: account, type: OathType.totp, challengeHex: challenge, - ), - ); - final calculationBytes = _decodeHex(calculation.data, 'OATH calculation'); - _expect( - calculationBytes.length == 7 && - calculationBytes[0] == 0x76 && - calculationBytes[1] == 5, - 'Unexpected OATH calculation response', - ); + ); + _expect(digits == 6, 'Unexpected OATH calculation digits'); - final listed = ApduResponse.parse( - await _oathClient.calculateAll(challenge), - ); - final accountTlv = _tlv(0x71, utf8.encode(account)); - _expect( - _containsBytes( - _decodeHex(listed.data, 'OATH calculation list'), - accountTlv, - ), - 'OATH calculation list does not contain the added account', - ); + final listed = await _oathClient.calculateAll(challenge); + _expect( + listed.any((entry) => entry.name == account), + 'OATH calculation list does not contain the added account', + ); - response = ApduResponse.parse(await _oathClient.delete(account)); - _expect( - response.statusWord == '9000', 'Console failed to delete OATH account'); - final empty = ApduResponse.parse( - await _oathClient.calculateAll(challenge), - ); - _expect( - !_containsBytes( - _decodeHex(empty.data, 'empty OATH calculation list'), - accountTlv, - ), - 'OATH account was not deleted', - ); - stdout.writeln('ok: oath.client.add_calculate_delete'); + await _oathClient.delete(account); + final empty = await _oathClient.calculateAll(challenge); + _expect( + !empty.any((entry) => entry.name == account), + 'OATH account was not deleted', + ); + stdout.writeln('ok: oath.client.add_calculate_delete'); + }); } - Future _ndefApplet() async { + Future _ndefApplet() => _ndefClient.withSession(() async { final initial = await _ndefClient.read(); _expect(initial != null, 'Console could not select the NDEF applet'); _expect( @@ -857,7 +879,9 @@ class ConsoleSmoke { stdout.writeln('ok: ndef.client.read_write'); await _selectAdminAndVerify(); - await _adminClient.setNdefReadOnly(true); + await _withAdmin( + () => _adminClient.setNdefReadOnly(true, pin: _defaultAdminPin), + ); var rejectedReadOnlyWrite = false; try { await _ndefClient.write(payload); @@ -867,15 +891,20 @@ class ConsoleSmoke { _expect(rejectedReadOnlyWrite, 'Console accepted a read-only NDEF write'); await _selectAdminAndVerify(); - await _adminClient.setNdefReadOnly(_initialNdefReadonly); - await _adminClient.resetNdef(); + await _withAdmin( + () => _adminClient.setNdefReadOnly( + _initialNdefReadonly, + pin: _defaultAdminPin, + ), + ); + await _withAdmin(() => _adminClient.resetNdef(pin: _defaultAdminPin)); final reset = await _ndefClient.read(); _expect( reset != null && _bytesEqual(reset.message, initialMessage), 'Console did not observe the initial NDEF content after reset', ); stdout.writeln('ok: ndef.client.readonly_and_reset'); - } + }); Future _webAuthnApplet() async { await _send('webauthn.select', '00A4040008A0000006472F0001'); @@ -931,69 +960,98 @@ class ConsoleSmoke { return; } - await _selectAdminAndVerify(); - var slots = await _passClient.readSlots(); - _expect(slots.length == 2, 'Expected two Pass slots'); + await _passClient.withSession(() async { + Future> readSlots() async { + await _passClient.prepare(); + return _passClient.readSlots(); + } - const password = 'console-usbip'; - _expect( - await _passClient.setSlot(1, PassSlotType.static, password, true), - 'Console failed to configure the short Pass slot', - ); - slots = await _passClient.readSlots(); - _expect( - slots[0].type == PassSlotType.static && slots[0].withEnter, - 'Pass short slot did not store the static password configuration', - ); + Future setSlot( + int index, + PassSlotType type, + String password, + bool withEnter, + ) async { + await _passClient.prepare(); + return _passClient.setSlot( + index, + type, + password, + withEnter, + pin: _defaultAdminPin, + ); + } - _expect( - await _passClient.setSlot(2, PassSlotType.static, password, false), - 'Console failed to configure the long Pass slot', - ); - slots = await _passClient.readSlots(); - _expect( - slots[1].type == PassSlotType.static && !slots[1].withEnter, - 'Pass long slot did not store the static password configuration', - ); + var slots = await readSlots(); + _expect(slots.length == 2, 'Expected two Pass slots'); - if (_functionSet == FunctionSetVersion.v5) { - final secret = _encodeHex( - List.generate(20, (index) => 0xA0 + index), + const password = 'console-usbip'; + _expect( + await setSlot(1, PassSlotType.static, password, true), + 'Console failed to configure the short Pass slot', ); + slots = await readSlots(); _expect( - await _passClient.setSlot(1, PassSlotType.hmacSha1, secret, false), - 'Console failed to configure the HMAC-SHA1 Pass slot', + slots[0].type == PassSlotType.static && slots[0].withEnter, + 'Pass short slot did not store the static password configuration', ); - slots = await _passClient.readSlots(); + _expect( - slots[0].type == PassSlotType.hmacSha1, - 'Pass short slot did not store the HMAC-SHA1 configuration', + await setSlot(2, PassSlotType.static, password, false), + 'Console failed to configure the long Pass slot', + ); + slots = await readSlots(); + _expect( + slots[1].type == PassSlotType.static && !slots[1].withEnter, + 'Pass long slot did not store the static password configuration', ); - } - _expect( - await _passClient.setSlot(1, PassSlotType.none, '', false), - 'Console failed to clear the short Pass slot', - ); - _expect( - await _passClient.setSlot(2, PassSlotType.none, '', false), - 'Console failed to clear the long Pass slot', - ); - slots = await _passClient.readSlots(); - _expect( - slots.every((slot) => slot.type == PassSlotType.none), - 'Pass slots were not cleared', - ); + if (_functionSet == FunctionSetVersion.v5) { + final secret = _encodeHex( + List.generate(20, (index) => 0xA0 + index), + ); + _expect( + await setSlot(1, PassSlotType.hmacSha1, secret, false), + 'Console failed to configure the HMAC-SHA1 Pass slot', + ); + slots = await readSlots(); + _expect( + slots[0].type == PassSlotType.hmacSha1, + 'Pass short slot did not store the HMAC-SHA1 configuration', + ); + } + + _expect( + await setSlot(1, PassSlotType.none, '', false), + 'Console failed to clear the short Pass slot', + ); + _expect( + await setSlot(2, PassSlotType.none, '', false), + 'Console failed to clear the long Pass slot', + ); + slots = await readSlots(); + _expect( + slots.every((slot) => slot.type == PassSlotType.none), + 'Pass slots were not cleared', + ); + }); stdout.writeln('ok: pass.client.configure_and_clear'); } Future _selectAdminAndVerify() async { - await _adminClient.select(); - _expect( - await _adminClient.verifyPin(_defaultAdminPin), - 'Console failed to verify the admin PIN', - ); + await _withAdmin(() async { + _expect( + await _adminClient.verifyPin(_defaultAdminPin), + 'Console failed to verify the admin PIN', + ); + }); } + + Future _withAdmin(Future Function() action) => + _adminClient.withSession(() async { + await _adminClient.prepare(); + return action(); + }); } class _CcidApduTransport implements ApduTransport { @@ -1113,25 +1171,6 @@ String _encodeHex(Iterable values) { return values.map(_hexByte).join(); } -List _tlv(int tag, List value) { - return [tag, value.length, ...value]; -} - -bool _containsBytes(List data, List needle) { - if (needle.isEmpty) return true; - for (var offset = 0; offset <= data.length - needle.length; offset++) { - var matches = true; - for (var index = 0; index < needle.length; index++) { - if (data[offset + index] != needle[index]) { - matches = false; - break; - } - } - if (matches) return true; - } - return false; -} - bool _bytesEqual(List left, List right) { if (left.length != right.length) return false; for (var index = 0; index < left.length; index++) { diff --git a/test/views/applets/settings/settings_page_test.dart b/test/views/applets/settings/settings_page_test.dart index 0a6d951..002c53c 100644 --- a/test/views/applets/settings/settings_page_test.dart +++ b/test/views/applets/settings/settings_page_test.dart @@ -1,5 +1,7 @@ import 'package:canokey_console/helper/storage/local_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:platform_detector/platform_detector.dart'; +import 'package:platform_detector/enums.dart'; import 'package:canokey_console/controller/applets/settings/settings_controller.dart'; import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/theme/app_theme.dart'; @@ -18,6 +20,14 @@ class _SettingsController extends SettingsController { void onReady() {} } +class _LifecycleSettingsController extends SettingsController { + int refreshes = 0; + @override + Future doRefreshData() async { + refreshes++; + } +} + void main() { setUp(() async { SharedPreferences.setMockInitialValues({}); @@ -33,6 +43,32 @@ void main() { }); tearDown(Get.reset); + testWidgets( + 'web settings refresh on each page entry with a retained controller', + (tester) async { + final platform = PlatformDetector.platform; + final previousType = platform.type; + platform.type = PlatformType.web; + addTearDown(() => platform.type = previousType); + final controller = + Get.put( + _LifecycleSettingsController(), + permanent: true, + ) + as _LifecycleSettingsController; + await tester.pumpWidget(_app()); + await tester.pumpAndSettle(); + expect(controller.refreshes, 1); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); + await tester.pumpWidget(_app()); + await tester.pumpAndSettle(); + expect(controller.refreshes, 2); + await tester.pumpWidget(const SizedBox.shrink()); + await Get.delete(force: true); + }, + ); + testWidgets('settings adapt to desktop, mobile, dark mode and large text', ( tester, ) async { From 7085eb76d10fad4fb17c5a5315abb9ec4d73f1b3 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 14:44:09 +0800 Subject: [PATCH 03/19] fix: authenticate pass slot reads on gated firmware INS 43 sits behind the admin applet's authentication gate on 3.x firmware, so pass slot reads now accept the lease's verified PIN as a fallback when no recorded Admin session evidence is available. --- lib/controller/applets/pass/pass_controller.dart | 2 +- lib/helper/utils/pass_card.dart | 6 +++++- test/helper/utils/pass_card_test.dart | 16 +++++++++++++++- test/usbip/console_smoke.dart | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/controller/applets/pass/pass_controller.dart b/lib/controller/applets/pass/pass_controller.dart index f46066d..7495a34 100644 --- a/lib/controller/applets/pass/pass_controller.dart +++ b/lib/controller/applets/pass/pass_controller.dart @@ -139,7 +139,7 @@ class PassController extends PollingController with AdminApplet { } Future _refresh() async { - slots = await _client.readSlots(); + slots = await _client.readSlots(pin: adminPinForCurrentLease); assert(slots.length == 2); polled = true; diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index b644c3b..1e82199 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -157,10 +157,14 @@ class PassCardClient { } } - Future> readSlots() async { + /// Reads both slots. Firmware gates INS 43 behind Admin authentication, so + /// callers pass the lease's verified PIN; a still-valid recorded Admin + /// authentication takes precedence and the PIN is not sent at all. + Future> readSlots({String? pin}) async { final result = await _execute( (profile, pinBytes, existing) => profile.adminPassSlots(pin: pinBytes, existing: existing), + pin: pin, ); return PassSlot.decode(result.data); } diff --git a/test/helper/utils/pass_card_test.dart b/test/helper/utils/pass_card_test.dart index 3dc9f47..64642b3 100644 --- a/test/helper/utils/pass_card_test.dart +++ b/test/helper/utils/pass_card_test.dart @@ -41,7 +41,7 @@ void main() { expect(unknown[0].type, PassSlotType.unknown); expect(unknown[1].type, PassSlotType.none); - // The unprotected read SELECTs but never verifies a PIN. + // A PIN-less read SELECTs but never verifies. expect(transport.commands, [ _select, '0043000000', @@ -52,6 +52,20 @@ void main() { }, ); + test('reads verify the supplied PIN on gated firmware', () async { + final transport = _Transport([ + '9000', + '9000', + // Both slots off. + '00009000', + ]); + await _prepared(transport, (client) async { + final slots = await client.readSlots(pin: '123456'); + expect(slots.every((slot) => slot.type == PassSlotType.none), isTrue); + expect(transport.commands, [_select, _verify, '0043000000']); + }); + }); + test('malformed slot dumps fail instead of fabricating slots', () async { final transport = _Transport(['9000', '029000']); await _prepared(transport, (client) async { diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index 814da71..795378b 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -963,7 +963,7 @@ class ConsoleSmoke { await _passClient.withSession(() async { Future> readSlots() async { await _passClient.prepare(); - return _passClient.readSlots(); + return _passClient.readSlots(pin: _defaultAdminPin); } Future setSlot( From ed320c773f5861840015fb627645cb7033ece06d Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 15:10:30 +0800 Subject: [PATCH 04/19] fix: authenticate the gated algorithm-extension read on firmware 3.0.x Firmware 3.0.x requires PIV management-key authentication for the INS EE read and resets that status on every SELECT. readAlgorithmExtensions now accepts an optional management key (select, prepare, authenticate and read in one selection), the capabilities refresh keeps its defaults fallback for exactly that 6982 gate, and pass slot reads supply the verified lease PIN on firmware gating INS 43. --- docs/libcanokey-migration.md | 8 +++- .../applets/piv/piv_controller.dart | 15 ++++++- lib/helper/utils/piv_card.dart | 18 ++++++++- test/helper/utils/piv_card_test.dart | 39 +++++++++++++++++++ test/usbip/console_smoke.dart | 11 +++++- 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 3ca0bd1..8843e30 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -70,8 +70,12 @@ The C ABI is unnecessary in Console's existing Rust library. - Errors retain category, phase, status word, credential reference and retry count as separate fields. Transport errors are propagated without conversion. - Only an unsupported algorithm-configuration instruction permits the existing - firmware defaults. Security failures, malformed responses and communication - errors no longer silently choose default algorithms. + firmware defaults, plus one firmware-scoped exception: 3.0.x gates that read + behind management-key authentication (every SELECT resets the status), so an + unauthenticated capabilities read takes the defaults fallback exactly on + 3.0.x, and `readAlgorithmExtensions` accepts an optional management key to + authenticate within the same selection. Other security failures, malformed + responses and communication errors never silently choose default algorithms. - OpenPGP card-info reads, PW1/PW3 credential operations and all administrative writes (reset code set/clear, retry limits, signature PIN diff --git a/lib/controller/applets/piv/piv_controller.dart b/lib/controller/applets/piv/piv_controller.dart index dc7150a..efd3126 100644 --- a/lib/controller/applets/piv/piv_controller.dart +++ b/lib/controller/applets/piv/piv_controller.dart @@ -159,7 +159,20 @@ class PivController extends PollingController { } Future _refreshCapabilities() async { - final config = await _client.readAlgorithmExtensions(); + PivAlgorithmExtensionConfig? config; + try { + config = await _client.readAlgorithmExtensions(); + } on ProtocolException catch (error) { + // Firmware 3.0.x gates this read behind management-key authentication + // (6982) and Console reads capabilities before any authentication, so + // keep the firmware-defaults fallback for exactly that known gate; + // every other failure remains visible. + final gatedByManagementKey = + error.details.kind == 'SecurityStatusNotSatisfied' && + firmwareVersion.compareTo(const FirmwareVersion(3, 0, 0)) >= 0 && + firmwareVersion.compareTo(const FirmwareVersion(3, 1, 0)) < 0; + if (!gatedByManagementKey) rethrow; + } if (config != null) { algorithmExtensionConfig = config; return; diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index 1bb7834..ba0e654 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -510,8 +510,24 @@ class PivCardClient { } } - Future readAlgorithmExtensions() async { + /// Reads the algorithm extension configuration. Firmware 3.0.x gates this + /// read behind management-key authentication and every SELECT resets that + /// status, so callers on such firmware pass [managementKey]: the client + /// SELECTs, prepares and authenticates in the same selection before reading. + Future readAlgorithmExtensions({ + String? managementKey, + AlgorithmType managementKeyAlgorithm = AlgorithmType.tdes, + }) async { await select(); + if (managementKey != null) { + await prepare(); + if (!await authenticateManagementKey( + managementKey, + managementKeyAlgorithm, + )) { + throw StateError('PIV management key authentication failed'); + } + } try { final data = await _read(PivReadOperation.algorithmConfiguration); return PivAlgorithmExtensionConfig.decode(data); diff --git a/test/helper/utils/piv_card_test.dart b/test/helper/utils/piv_card_test.dart index fc5fc4d..31d9ccd 100644 --- a/test/helper/utils/piv_card_test.dart +++ b/test/helper/utils/piv_card_test.dart @@ -659,6 +659,45 @@ void main() { }, ); + test( + 'algorithm configuration authenticates the management key when supplied', + () async { + // 3.0.x firmware gates the read behind management-key authentication. + const firmware = '3.0.3'; + final probe = [..._probeResponses]; + probe[1] = '${hex.encode(firmware.codeUnits)}9000'; + final transport = _QueueApduTransport([ + '9000', // explicit SELECT + ...probe, + '7C0A8108FEDCBA98765432109000', // management challenge + '9000', // management authentication response + '01E00516E1531554E2E39000', // extension configuration + ]); + final client = PivCardClient(transport: transport); + await client.withSession(() async { + final config = await client.readAlgorithmExtensions( + managementKey: '0123456789abcdef23456789abcdef01456789abcdef0123', + ); + expect(config, isNotNull); + expect(config!.enabled, isTrue); + // No SELECT between the authentication and the gated read. + expect(transport.commands, [ + '00A4040005A00000030800', + '00A4040005F00000000000', + '0031000000', + '0031010000', + '0032000000', + '00A4040005A00000030800', + '00FD000000', + '00EE010000', + '0087039B047C02810000', + '0087039B0C7C0A82080737F6C53750D4A400', + '00EE010000', + ]); + }); + }, + ); + test('reads optional algorithm extensions and missing metadata', () async { final config = await PivCardClient( transport: _QueueApduTransport(['9000', '01E00516E1531554E2E39000']), diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index 795378b..665535f 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -24,6 +24,8 @@ import 'package:flutter_test/flutter_test.dart'; const _defaultAdminPin = '123456'; const _defaultPivPin = '123456'; const _alternatePivPin = '654321'; +const _defaultPivManagementKey = + '010203040506070801020304050607080102030405060708'; const _defaultOpenPgpUserPin = '123456'; const _alternateOpenPgpUserPin = '654321'; const _defaultOpenPgpAdminPin = '12345678'; @@ -773,7 +775,14 @@ class ConsoleSmoke { } } - await _pivClient.readAlgorithmExtensions(); + // Firmware 3.0.x gates the extension-config read behind management-key + // authentication; newer firmware allows it unauthenticated. + final gatesExtensionRead = + firmware.compareTo(const FirmwareVersion(3, 0, 0)) >= 0 && + firmware.compareTo(const FirmwareVersion(3, 1, 0)) < 0; + await _pivClient.readAlgorithmExtensions( + managementKey: gatesExtensionRead ? _defaultPivManagementKey : null, + ); stdout.writeln('ok: piv.client.read_change_and_restore'); }); } From 4ca1cc5b1f0dc984deaed632acffb085b5cbc117 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 16:25:17 +0800 Subject: [PATCH 05/19] feat: adopt upstream CTAP2 client and consolidate card clients WebAuthn now runs on libcanokey's ctap2/ClientPin/credmgmt operations via new facade bindings, replacing the fido2 package, the fido2_crypto crate, its web WASM backend and the CTAP transmitter. Card clients share card_client.dart (ProfileBinding / CardClientBase / ProfileCardClient / AdminSessionCardClient), removing ~470 lines of duplicated boilerplate. --- .github/workflows/deploy.yml | 14 - .github/workflows/usbip.yml | 3 +- .gitignore | 1 - README.md | 11 +- docs/libcanokey-migration.md | 118 +- .../applets/pass/pass_controller.dart | 4 +- .../applets/webauthn/webauthn_controller.dart | 195 +-- lib/controller/base/admin.dart | 2 +- lib/helper/utils/admin_card.dart | 222 +-- lib/helper/utils/card_client.dart | 230 +++ lib/helper/utils/ctap_transmitter.dart | 72 - lib/helper/utils/fido2_backend.dart | 2 - lib/helper/utils/fido2_backend_native.dart | 27 - lib/helper/utils/fido2_backend_web.dart | 4 - lib/helper/utils/ndef_card.dart | 52 +- lib/helper/utils/oath_card.dart | 150 +- lib/helper/utils/openpgp_card.dart | 146 +- lib/helper/utils/pass_card.dart | 177 +-- lib/helper/utils/piv_card.dart | 250 +--- lib/helper/utils/protocol_operation.dart | 54 +- lib/helper/utils/webauthn_card.dart | 375 +++++ lib/main.dart | 3 - lib/models/webauthn.dart | 4 +- lib/src/rust/api/protocol.dart | 91 +- lib/src/rust/frb_generated.dart | 1326 +++++++++++++++-- lib/src/rust/frb_generated.io.dart | 304 ++++ lib/src/rust/frb_generated.web.dart | 288 ++++ pubspec.lock | 24 - pubspec.yaml | 2 - rust/Cargo.lock | 186 +-- rust/Cargo.toml | 11 +- rust/THIRD_PARTY_LICENSES.json | 102 +- rust/src/api/piv_crypto.rs | 118 +- rust/src/api/protocol.rs | 864 +++++++++++ rust/src/frb_generated.rs | 1040 +++++++++++-- rust/src/lib.rs | 12 - .../webauthn/ctap_transmitter_test.dart | 131 -- test/helper/utils/admin_card_test.dart | 4 +- test/helper/utils/fido2_backend_test.dart | 109 -- test/helper/utils/pass_card_test.dart | 8 +- test/helper/utils/webauthn_card_test.dart | 369 +++++ test/usbip/console_smoke.dart | 49 +- .../applets/webauthn/webauthn_page_test.dart | 7 +- web/index.html | 1 - 44 files changed, 5231 insertions(+), 1931 deletions(-) create mode 100644 lib/helper/utils/card_client.dart delete mode 100644 lib/helper/utils/ctap_transmitter.dart delete mode 100644 lib/helper/utils/fido2_backend.dart delete mode 100644 lib/helper/utils/fido2_backend_native.dart delete mode 100644 lib/helper/utils/fido2_backend_web.dart create mode 100644 lib/helper/utils/webauthn_card.dart delete mode 100644 test/controller/applets/webauthn/ctap_transmitter_test.dart delete mode 100644 test/helper/utils/fido2_backend_test.dart create mode 100644 test/helper/utils/webauthn_card_test.dart diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2c2bf3b..05b36f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,20 +49,6 @@ jobs: cargo install wasm-pack --version "$WASM_PACK_VERSION" --locked flutter_rust_bridge_codegen build-web --release \ --wasm-pack-rustup-toolchain "$RUST_NIGHTLY" - - name: Restore FIDO2 Web backend - id: fido2_web_cache - uses: actions/cache@v5 - with: - path: web/fido2 - key: fido2-web-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_NIGHTLY }}-${{ steps.wasm_pack.outputs.version }}-${{ hashFiles('pubspec.lock') }} - - name: Build FIDO2 Web backend - if: steps.fido2_web_cache.outputs.cache-hit != 'true' - env: - RUSTUP_TOOLCHAIN: ${{ env.RUST_NIGHTLY }} - run: | - dart run fido2:setup --web --output=build/fido2 - mkdir -p web/fido2 - cp build/fido2/web/fido2_crypto* web/fido2/ - run: flutter build web --verbose - uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/usbip.yml b/.github/workflows/usbip.yml index cc1b71f..dd92234 100644 --- a/.github/workflows/usbip.yml +++ b/.github/workflows/usbip.yml @@ -62,7 +62,7 @@ jobs: rust-src-dir: rust cache-key: usbip-backend - - name: Build FIDO2 backend + - name: Build native backend if: steps.backend_cache.outputs.cache-hit != 'true' run: | cargo build --manifest-path rust/Cargo.toml --release --locked @@ -80,7 +80,6 @@ jobs: - name: Test native backends and client integration env: - FIDO2_CRYPTO_LIBRARY: ${{ github.workspace }}/build/usbip-backend/librust_lib_canokey_console.so FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR: ${{ github.workspace }}/build/usbip-backend run: flutter test --no-pub --tags native diff --git a/.gitignore b/.gitignore index f594e9b..db49113 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ build *.iml .flutter-plugins* dist/ -web/fido2/ app-store-screenshots/ android/fastlane/report.xml ios/fastlane/report.xml diff --git a/README.md b/README.md index 9adaa09..57689b9 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,6 @@ Visit our web application at [CanoKey Console Web](https://console.canokeys.org) 3. Run the application: ```bash flutter_rust_bridge_codegen build-web --release --wasm-pack-rustup-toolchain nightly-2026-09-04 # for web only - dart run fido2:setup --web --output=build/fido2 # for web only - mkdir -p web/fido2 && cp build/fido2/web/fido2_crypto* web/fido2/ # for web only flutter run ``` @@ -99,11 +97,6 @@ If you change any Rust dependencies (`Cargo.lock`), please run: cd rust && cargo bundle-licenses -f json | jq '.third_party_libraries | del(.[].licenses)' > THIRD_PARTY_LICENSES.json ``` -The fido2 Dart package and native Rust revision are pinned together at 2.0.0. -Its native C ABI is linked into the existing console Rust library. The -wasm-bindgen dependency family is pinned to versions compatible with fido2 2.0.0. -Web uses the separate JS/WASM loader distributed with the Dart package. - PIV selection, version and algorithm-configuration reads now use a pinned libcanokey Rust dependency through Flutter Rust Bridge. See the [migration boundary and next steps](docs/libcanokey-migration.md) for the @@ -113,15 +106,13 @@ Before running backend or USB/IP tests locally, build the native backend: ```bash cargo build --manifest-path rust/Cargo.toml --release --locked -flutter test test/helper/utils/fido2_backend_test.dart +flutter test --tags native ``` ### Web ```bash flutter_rust_bridge_codegen build-web --release --wasm-pack-rustup-toolchain nightly-2026-09-04 -dart run fido2:setup --web --output=build/fido2 -mkdir -p web/fido2 && cp build/fido2/web/fido2_crypto* web/fido2/ flutter build web ``` diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 8843e30..baa6739 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -117,12 +117,27 @@ The C ABI is unnecessary in Console's existing Rust library. write capability, and 6982 maps to `NdefReadOnlyException`. All manual chunking, NLEN and capability-container parsing was removed from `ndef_card.dart`. -- CTAP/WebAuthn transport is a thin layer over `ctapSelectApplication` plus - `ctapTransceiveSelected`; selection evidence is tracked per lease and a new - lease reselects automatically. All six raw SELECT FIDO2 AID sites in - webauthn_controller, the manual 80100000 envelope and the 80C00000 - continuation loop were removed. Non-zero CTAP statuses pass through to the - fido2 package as data; CBOR/ClientPin framing stays in Dart. +- CTAP/WebAuthn is fully upstream: the CTAP2 client-layer bindings cover every + WebAuthn management use case — `ctapGetInfo` (options tri-states, + forcePinChange, minPinLength and advertised pinUvAuthProtocol versions + parsed in Rust), ClientPIN `ctapBeginPinSession`/`setPin`/`changePin`/ + `getPinTokenWithPermissions`, and credentialManagement `enumerateRps`/ + `enumerateCredentials` (including the CanoKey metadataOnly 0x80 extension)/ + `deleteCredential`. PinSession and PinToken are opaque FRB handles holding + zeroizing Rust state; ephemeral scalars and V2 IVs are generated in the + facade from the CSPRNG (`rand`), never supplied by Dart. For CTAP-level + failures `ProtocolError.statusWord` carries the raw CTAP status byte widened + to u16, not an ISO 7816 status word. Enumeration results use self-delimiting + byte encodings; Dart never parses CBOR. The Dart `WebAuthnCardClient` + (`lib/helper/utils/webauthn_card.dart`) decodes those encodings, prefers + pinUvAuthProtocol V2 when advertised, and owns session/token handle + lifecycles; every operation SELECTs the FIDO2 applet itself, so consecutive + operations in one use case never assume a residual selection. Tokens are + ceremony-scoped: each use case (enumeration, deletion) mints a fresh one + instead of caching the handle, and PIN set/change is followed by a fresh + getInfo. The Dart fido2 package, the `fido2_crypto` crate (and its C ABI in + `lib.rs`), the `web/fido2` WASM artifacts and the raw CTAP transmitter layer + were removed; no Dart CBOR/ClientPin framing remains. - Pass reads/writes use `adminPassSlots` (read, not PIN-protected) and `adminSetPassSlot` (protected) with the same thin client shape as admin_card; both reuse the lease's verified Admin session when one is @@ -135,11 +150,14 @@ The C ABI is unnecessary in Console's existing Rust library. request path (`adminRead`/`adminConfigure`/`adminAction` and the PASS slot operations), `pivImportPqSeed`, `oathSetDefault`, `ndefReadCapability`/ `ndefReadMessage`/`ndefWriteMessage` and `ctapSelectApplication`/ - `ctapTransceiveSelected`/`ctapTransceive`. + `ctapTransceiveSelected`/`ctapTransceive`. The CTAP2 client-layer increment + adds `ctapGetInfo`, `ctapBeginPinSession`, the `CtapPinSession` methods + `protocolVersion`/`setPin`/`changePin`/`getPinTokenWithPermissions`/`close` + and the `CtapPinToken` methods `protocolVersion`/`enumerateRps`/ + `enumerateCredentials`/`deleteCredential`/`close`. Existing UI models still map validated configuration bytes to display/algorithm -fields. The WebAuthn CBOR/ClientPin layer and the keyboard keymap commands remain -in Dart. Admin coverage and the specific upstream gaps are described below. +fields. Admin coverage and the specific upstream gaps are described below. ## Admin integration @@ -219,10 +237,12 @@ order. The result variant comes from the probed firmware, not guessed reply leng evidence (see Admin integration above) and otherwise keep `existing: false` with an explicit per-request PIN. `Request::VerifyPin` never uses Existing (upstream rejects it), so the explicit authentication step is unchanged. -- **WebAuthn CBOR/ClientPin:** the CBOR framing and ClientPin protocol (fido2 - package) stay in Dart as a host-side policy choice, not an upstream gap. - Only the CTAP transport envelope (80100000) and the 80C00000 continuation - loop moved upstream. +- **WebAuthn CBOR/ClientPin:** converged. The controller migrated off the + fido2 package onto the facade's CTAP2 client layer (`ctapGetInfo`, + `ctapBeginPinSession`, `setPin`/`changePin`/`getPinTokenWithPermissions`, + `enumerateRps`/`enumerateCredentials`/`deleteCredential`); no Dart-side + CBOR or ClientPin framing remains, and the `fido2_crypto` crate and its C + ABI were removed from `lib.rs`. - **Keyboard keymap read/write/reset (45/46/47):** migrated; these commands are routed through upstream Admin operations (see the existing-access increment below). No Dart-side raw keymap exchanges remain. @@ -325,11 +345,12 @@ hand-built APDUs. flutter_rust_bridge_codegen generate cargo test --manifest-path rust/Cargo.toml api::protocol --locked cargo build --manifest-path rust/Cargo.toml --release --locked -flutter test --no-pub test/helper/utils/piv_card_test.dart test/helper/utils/fido2_backend_test.dart +flutter test --no-pub test/helper/utils/piv_card_test.dart test/helper/utils/webauthn_card_test.dart flutter test --no-pub --exclude-tags native ``` -The native PIV transcript tests run alongside FIDO2 tests in the USB/IP workflow. +The native PIV transcript tests run alongside the WebAuthn CTAP2 tests in the +USB/IP workflow. The firmware smoke entry point initializes FRB before the migrated PIV operations. Native transcripts do not establish physical-device or legacy-firmware coverage. @@ -489,3 +510,70 @@ After this increment the FRB WASM package was regenerated again and `flutter build web --no-pub` passed, strict clippy is warning-free and the full `flutter test` run passed (381 tests). No physical card, browser transport or USB/IP firmware matrix was exercised for this increment. + +CTAP2 client-layer increment validation on 2026-09-16: `cargo test --locked` +passed in full (74 tests, including 8 new facade tests covering the getInfo +field encoding and tri-states, golden ClientPIN wire bytes for V1/V2 against +the upstream known answers, fresh-IV generation, pinUvAuthToken handles, +RP/credential enumeration encodings including the metadataOnly extension, +deleteCredential, and raw-CTAP-status error mapping). FRB 2.13.0 regeneration, +`cargo build --release --locked` and `cargo check --target +wasm32-unknown-unknown --locked` passed (`rand` moved to `[dependencies]`; +wasm entropy still comes from getrandom's wasm_js feature). Dart business +code is untouched; the controller migration is a follow-up increment. + +## Card-client boilerplate consolidation (2026-09-16) + +A pure-Dart, behavior-preserving refactoring after the applet migrations: the +six card clients now share `CardClientBase` (transport/lease guard, +`withSession`, cancellation) and `ProfileCardClient` (prepared-profile +ownership, generic `prepareProfile` and `executePrepared` with per-applet +flags for applet pre-selection, profile-identity checks and profile-discard +policy) in `lib/helper/utils/card_client.dart`, plus one `ProfileBinding` +class (lease + profile generation, optionally the selection generation for +PIV) replacing the five per-applet binding classes. Admin and Pass share +`AdminSessionCardClient` (in admin_card.dart) for the lease-evidenced +`Access::Existing` request path and Admin progress handling; NDEF stays +profile-free on `CardClientBase`. Admin/Pass `lastResponse` was renamed to +`lastStatusWord` for uniformity (the three controller call sites updated). +No protocol flow, lease/profile/selection generation check, error mapping or +public API semantics changed; the existing card-client, session, executor and +native transcript suites pin the behavior. `dart analyze` reports no issues +in the changed files, and the full `flutter test --no-pub` (383 tests) and +`flutter test --no-pub --tags native` (126 tests) runs passed. + +## WebAuthn CTAP2 client-layer migration (2026-09-16) + +The WebAuthn controller migrated off the Dart fido2 package onto the facade's +CTAP2 client layer. `lib/helper/utils/webauthn_card.dart` (a `CardClientBase`) +parses the getInfo/RP/credential byte encodings, owns the PIN session/token +handle lifecycles (fresh `beginPinSession` + `getPinTokenWithPermissions` per +use case, pinUvAuthProtocol V2 preferred when advertised) and marks every +operation as applet-selecting; `protocol_operation.dart` gained PIN +session/token executors. `_showPinError` recovers the PIN_INVALID (0x31) / +PIN_AUTH_BLOCKED (0x34) / PIN_BLOCKED (0x32) / PIN_POLICY_VIOLATION (0x37) +distinction from the raw CTAP status byte in `ProtocolError.statusWord`. +`WebAuthnItem.credentialId` is a local `Uint8List`. Removed: the fido2 2.0.0 +pub dependency, `fido2_backend*.dart`, `ctap_transmitter.dart`, the +`initializeFido2Backend` startup calls, the `web/fido2` WASM artifacts and +their `index.html` loader, the `fido2_crypto` crate and its retained C ABI in +`rust/src/lib.rs`, and the FIDO2 web backend steps in deploy.yml (the usbip +workflow's backend step no longer exports `FIDO2_CRYPTO_LIBRARY`). The +wasm-bindgen family pins stay, with the comment reworded; FRB bindings were +not regenerated (no facade change). + +Validation: `cargo test --locked` (74 tests), `cargo build --release --locked` +and `cargo check --target wasm32-unknown-unknown --locked` passed; +THIRD_PARTY_LICENSES.json regenerated without fido2_crypto. The new +`test/helper/utils/webauthn_card_test.dart` replays the upstream canokey-ctap +golden transcripts over injected transports (getInfo tri-states/minPinLength/ +protocols, V1/V2 key agreement, set/changePIN, token minting, RP/credential +enumeration encodings, deletion, empty-enumeration, closed-handle and +status-byte error mapping); the old fido2_backend/ctap_transmitter tests were +removed. `flutter test --no-pub` passed (387 tests, including the updated +WebAuthn page/model tests and the usbip smoke's new `webauthn.client.get_info` +section), `flutter test --no-pub --tags native` passed (130 tests), and +`dart analyze` reports no issues. `flutter build web --no-pub` passed with no +fido2 reference left in the web output. The FRB WASM package was not +regenerated (no Rust facade change); no physical card, browser transport or +USB/IP firmware matrix was exercised. diff --git a/lib/controller/applets/pass/pass_controller.dart b/lib/controller/applets/pass/pass_controller.dart index 7495a34..639b7b1 100644 --- a/lib/controller/applets/pass/pass_controller.dart +++ b/lib/controller/applets/pass/pass_controller.dart @@ -109,7 +109,7 @@ class PassController extends PollingController with AdminApplet { pin: pin, ); } on ProtocolException { - if (Prompts.isStorageFull(_client.lastResponse ?? '')) { + if (Prompts.isStorageFull(_client.lastStatusWord ?? '')) { Prompts.showPrompt( S.of(Get.context!).storageFull, ContentThemeColor.danger, @@ -119,7 +119,7 @@ class PassController extends PollingController with AdminApplet { rethrow; } if (!success) { - Prompts.promptPinFailureResult(_client.lastResponse ?? ''); + Prompts.promptPinFailureResult(_client.lastStatusWord ?? ''); return; } log.i('Successfully changed slot'); diff --git a/lib/controller/applets/webauthn/webauthn_controller.dart b/lib/controller/applets/webauthn/webauthn_controller.dart index e8bc2ad..24a30b4 100644 --- a/lib/controller/applets/webauthn/webauthn_controller.dart +++ b/lib/controller/applets/webauthn/webauthn_controller.dart @@ -6,30 +6,33 @@ import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; import 'package:canokey_console/helper/theme/admin_theme.dart'; import 'package:canokey_console/helper/utils/applet_switches.dart'; -import 'package:canokey_console/helper/utils/ctap_transmitter.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/utils/prompts.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/helper/utils/webauthn_card.dart'; import 'package:canokey_console/helper/widgets/input_pin_dialog.dart'; import 'package:canokey_console/helper/widgets/validators.dart'; import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/webauthn.dart'; import 'package:canokey_console/views/applets/webauthn/dialogs/force_pin_change_dialog.dart'; -import 'package:fido2/fido2.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:logger/logger.dart'; class WebAuthnController extends PollingController with AdminApplet { - late Ctap2 _ctap; - final CtapTransmitter _transmitter = CtapTransmitter(); + final WebAuthnCardClient _client = WebAuthnCardClient(); final _localPinCache = CredentialCache('webauthn'); final List webAuthnItems = []; FirmwareVersion firmwareVersion = const FirmwareVersion(0, 0, 0); FunctionSetVersion functionSetVersion = FunctionSetVersion.v1; String? disabledMessage; + /// Display state from the latest getInfo; refreshed after PIN mutations. + WebAuthnInfo? _info; + bool get supportsSm2Settings => CanoKey.functionSet(functionSetVersion).contains(Func.webAuthnSm2Support); @@ -53,37 +56,28 @@ class WebAuthnController extends PollingController with AdminApplet { } disabledMessage = null; - List? pinToken = await _getPinToken(sn); + WebAuthnPinToken? pinToken = await _getPinToken(sn); if (pinToken == null) { return; } webAuthnItems.clear(); - final cp = ClientPin(_ctap); - final cm = CredentialManagement( - _ctap, - cp.pinProtocolVersion == 1 ? PinProtocolV1() : PinProtocolV2(), - pinToken); try { - for (var rp in (await cm.enumerateRPs())) { - for (var element - in (await cm.enumerateCredentialsMetadataOnly(rp.rpIdHash))) { + for (var rp in await pinToken.enumerateRps()) { + for (var credential + in await pinToken.enumerateCredentials(rp.idHash)) { webAuthnItems.add(WebAuthnItem( - rpId: rp.rp.id, - userName: element.user.name ?? '', - userDisplayName: element.user.displayName ?? '', - userId: element.user.id, - credentialId: element.credentialId, + rpId: rp.id, + userName: credential.userName ?? '', + userDisplayName: credential.userDisplayName ?? '', + userId: credential.userId ?? const [], + credentialId: credential.credentialId, )); } } - } on CtapError catch (e) { - if (e.status == CtapStatusCode.ctap2ErrNoCredentials) { - log.i('No credentials'); - } else { - rethrow; - } + } finally { + pinToken.close(); } polled = true; @@ -100,19 +94,16 @@ class WebAuthnController extends PollingController with AdminApplet { return; } - await _transmitter.selectApplication(); - final cp = ClientPin(_ctap); + final session = await _client.beginPinSession(await _refreshInfo()); try { - await cp.changePin(pinToTry, newPin); - } catch (e) { - if (e is CtapError) { - _showPinError(e.status); - return; - } else { - rethrow; - } + await session.changePin(pinToTry, newPin); + } on ProtocolException catch (e) { + _showPinError(e); + return; + } finally { + session.close(); } - + await _refreshInfo(); await _setPinCache(sn, newPin, savePin); log.i('Successfully changed PIN'); @@ -165,7 +156,7 @@ class WebAuthnController extends PollingController with AdminApplet { }); } - Future delete(PublicKeyCredentialDescriptor credentialId) async { + Future delete(List credentialId) async { log.t('Call WebAuthnController.delete'); await SmartCard.process((String sn) async { String? pinToTry = _loadPin(sn); @@ -174,22 +165,22 @@ class WebAuthnController extends PollingController with AdminApplet { return; } - await _transmitter.selectApplication(); - final cp = ClientPin(_ctap); - final pinToken = await cp.getPinToken(pinToTry, - permissions: [ClientPinPermission.credentialManagement]); - final cm = CredentialManagement( - _ctap, - cp.pinProtocolVersion == 1 ? PinProtocolV1() : PinProtocolV2(), - pinToken); - await cm.deleteCredential(credentialId); + final pinToken = await _doGetPinToken(pinToTry); + if (pinToken == null) { + return; + } + try { + await pinToken.deleteCredential(credentialId); + } finally { + pinToken.close(); + } log.i('Successfully deleted credential'); Navigator.pop(Get.context!); Prompts.showPrompt(S.of(Get.context!).delete, ContentThemeColor.success, forceSnackBar: true); - webAuthnItems - .removeWhere((element) => element.credentialId == credentialId); + webAuthnItems.removeWhere( + (element) => listEquals(element.credentialId, credentialId)); update(); }); } @@ -210,28 +201,29 @@ class WebAuthnController extends PollingController with AdminApplet { return null; } - Future?> _getPinToken(String sn) async { - await _transmitter.selectApplication(); - _ctap = await Ctap2.create(_transmitter); + Future _refreshInfo() async => + _info = await _client.getInfo(); + + Future _getPinToken(String sn) async { + final info = await _refreshInfo(); // We do nothing if the device does not support credMgmt or clientPin - if (_ctap.info.options?['credMgmt'] != true || - _ctap.info.options?['clientPin'] == null) { + if (info.credMgmt != true || info.clientPin == null) { Prompts.showPrompt(S.of(Get.context!).webauthnClientPinNotSupported, ContentThemeColor.danger); return null; } // If PIN is not set, ask the user to set PIN first - if (_ctap.info.options?['clientPin'] == false) { + if (info.clientPin == false) { if (!await _setPin(sn)) { return null; } } - assert(_ctap.info.options?['clientPin'] == true); + assert(_info!.clientPin == true); - if (_ctap.info.forcePinChange == true) { + if (_info!.forcePinChange == true) { return _forceChangePin(sn); } @@ -259,7 +251,7 @@ class WebAuthnController extends PollingController with AdminApplet { // Finally, prompt user // When using NFC, we need to finish NFC before showing the dialog await SmartCard.stopPollingNfc(withInput: true); - final completer = Completer?>(); + final completer = Completer(); InputPinDialog.show( title: S.of(Get.context!).webauthnInputPinTitle, label: 'PIN', @@ -277,7 +269,7 @@ class WebAuthnController extends PollingController with AdminApplet { return; // timeout, do not close the dialog } Prompts.stopPromptAndroidPolling(); - List? pinToken; + WebAuthnPinToken? pinToken; try { pinToken = await _doGetPinToken(pin); } on PlatformException catch (e) { @@ -328,13 +320,16 @@ class WebAuthnController extends PollingController with AdminApplet { } Prompts.stopPromptAndroidPolling(); try { - // Set PIN and refresh by recreating Ctap2 - await _transmitter.selectApplication(); - final cp = ClientPin(_ctap); - await cp.setPin(pin); - // Update _ctap before continuing so later PIN-token operations see - // the authenticator's new clientPin state. - _ctap = await Ctap2.create(_transmitter); + final session = + await _client.beginPinSession(_info ?? await _refreshInfo()); + try { + await session.setPin(pin); + } finally { + session.close(); + } + // Refresh getInfo so later PIN-token operations see the + // authenticator's new clientPin state. + await _refreshInfo(); log.i('setPin success'); } on PlatformException catch (e) { await SmartCard.stopPollingNfc(withInput: true); @@ -368,23 +363,24 @@ class WebAuthnController extends PollingController with AdminApplet { return await completer.future; } - Future?> _doGetPinToken(String pin) async { + Future _doGetPinToken(String pin) async { + final session = + await _client.beginPinSession(_info ?? await _refreshInfo()); try { - await _transmitter.selectApplication(); - final cp = ClientPin(_ctap); - return await cp.getPinToken(pin, - permissions: [ClientPinPermission.credentialManagement]); - } on CtapError catch (e) { - _showPinError(e.status); + return await session.getPinToken(pin); + } on ProtocolException catch (e) { + _showPinError(e); return null; + } finally { + session.close(); } } - Future?> _forceChangePin(String sn) async { + Future _forceChangePin(String sn) async { await SmartCard.stopPollingNfc(withInput: true); - final completer = Completer?>(); + final completer = Completer(); await ForcePinChangeDialog.show( - minPinLength: _ctap.info.minPinLength ?? 4, + minPinLength: _info?.minPinLength ?? 4, onSubmit: (currentPin, newPin, savePin) async { SmartCard.nfcState = NfcState.processWithInput; if (!await SmartCard.pollNfcOrWebUsb()) { @@ -396,10 +392,13 @@ class WebAuthnController extends PollingController with AdminApplet { } Prompts.stopPromptAndroidPolling(); try { - await _transmitter.selectApplication(); - final cp = ClientPin(_ctap); - await cp.changePin(currentPin, newPin); - _ctap = await Ctap2.create(_transmitter); + final session = await _client.beginPinSession(_info!); + try { + await session.changePin(currentPin, newPin); + } finally { + session.close(); + } + await _refreshInfo(); final pinToken = await _doGetPinToken(newPin); if (pinToken == null) { await SmartCard.stopPollingNfc(withInput: true); @@ -413,9 +412,9 @@ class WebAuthnController extends PollingController with AdminApplet { Prompts.showPrompt( S.of(Get.context!).pinChanged, ContentThemeColor.success); return true; - } on CtapError catch (e) { + } on ProtocolException catch (e) { await SmartCard.stopPollingNfc(withInput: true); - _showPinError(e.status); + _showPinError(e); return false; } on PlatformException catch (e) { await SmartCard.stopPollingNfc(withInput: true); @@ -447,22 +446,26 @@ class WebAuthnController extends PollingController with AdminApplet { return completer.future; } - void _showPinError(CtapStatusCode status) { - if (status == CtapStatusCode.ctap2ErrPinInvalid) { - Prompts.showPrompt( - S.of(Get.context!).pinIncorrect, ContentThemeColor.danger); - } else if (status == CtapStatusCode.ctap2ErrPinAuthBlocked) { - Prompts.showPrompt( - S.of(Get.context!).webauthnPinAuthBlocked, ContentThemeColor.danger); - } else if (status == CtapStatusCode.ctap2ErrPinBlocked) { - Prompts.showPrompt( - S.of(Get.context!).webauthnPinBlocked, ContentThemeColor.danger); - } else if (status == CtapStatusCode.ctap2ErrPinPolicyViolation) { - Prompts.showPrompt( - S.of(Get.context!).changePinPrompt(_ctap.info.minPinLength ?? 4, 63), - ContentThemeColor.danger); - } else { - Prompts.showPrompt(S.current.pinVerificationFailed, ContentThemeColor.danger); + /// CTAP failures keep the raw CTAP status byte in [ProtocolError.statusWord]: + /// 0x31 PIN_INVALID, 0x32 PIN_BLOCKED, 0x34 PIN_AUTH_BLOCKED and 0x37 + /// PIN_POLICY_VIOLATION select the same prompts the fido2 statuses did. + void _showPinError(ProtocolException error) { + switch (error.details.statusWord) { + case 0x31: + Prompts.showPrompt( + S.of(Get.context!).pinIncorrect, ContentThemeColor.danger); + case 0x34: + Prompts.showPrompt( + S.of(Get.context!).webauthnPinAuthBlocked, ContentThemeColor.danger); + case 0x32: + Prompts.showPrompt( + S.of(Get.context!).webauthnPinBlocked, ContentThemeColor.danger); + case 0x37: + Prompts.showPrompt( + S.of(Get.context!).changePinPrompt(_info?.minPinLength ?? 4, 63), + ContentThemeColor.danger); + default: + Prompts.showPrompt(S.current.pinVerificationFailed, ContentThemeColor.danger); } } diff --git a/lib/controller/base/admin.dart b/lib/controller/base/admin.dart index 39c9938..e70e0f6 100644 --- a/lib/controller/base/admin.dart +++ b/lib/controller/base/admin.dart @@ -188,7 +188,7 @@ mixin AdminApplet on GetxController { }); return true; } else { - Prompts.promptPinFailureResult(adminCardClient.lastResponse ?? ''); + Prompts.promptPinFailureResult(adminCardClient.lastStatusWord ?? ''); return false; } } diff --git a/lib/helper/utils/admin_card.dart b/lib/helper/utils/admin_card.dart index 84f16c0..1c8f4b3 100644 --- a/lib/helper/utils/admin_card.dart +++ b/lib/helper/utils/admin_card.dart @@ -1,10 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/card_client.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/webauthn.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; @@ -16,125 +14,33 @@ class AdminStorageUsage { final int totalKiB; } -/// Each upstream Admin operation either SELECTs and explicitly authenticates -/// its own request, or reuses this lease's authenticated Admin session via -/// `Access::Existing`. That session evidence is recorded only by a successful -/// explicit [verifyPin] on the same lease and is bound to the lease's -/// selection/profile generations; a UI PIN cache is never authorization. -class AdminCardClient { - AdminCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } +/// Shared Admin-facade plumbing for clients whose upstream operations either +/// SELECT Admin and explicitly verify a PIN, or reuse this lease's recorded +/// Admin authentication via `Access::Existing`. That session evidence is +/// recorded only by a successful explicit VerifyPin on the same lease and is +/// bound to the lease's selection/profile generations; a UI PIN cache is never +/// authorization. +abstract class AdminSessionCardClient extends ProfileCardClient { + AdminSessionCardClient({super.transport, super.lease}) + : super(bindSelection: false); - final ApduTransport _transport; - final CardLease? _injectedLease; - final CardSessions _injectedSessions = CardSessions(); - final CardCancellation _cancellation = CardCancellation(); - _AdminProfileBinding? _profile; - String? lastResponse; + /// Progress of the most recent Admin operation, including after a failure. AdminProgress? lastProgress; - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected Admin transport requires withSession')); - - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } - - void cancelPendingOperations() => _cancellation.cancel(); - - /// Explicit minimal discovery, before any authentication. Repeat after a - /// profile-affecting write, including an uncertain write; never auto-reprobe. - Future prepare() async { - _cancellation.check(); - final lease = _lease; - lease.willSelectApplet(); - _profile?.close(); - _profile = null; - final profile = await executeProfileProbe( - ProtocolOperation.probeAdmin(), - _transport, - lease: lease, - cancellation: _cancellation, - ); - final binding = _AdminProfileBinding(profile, lease); - try { - lease.check(); - _cancellation.check(); - lease.onClose(() { - binding.close(); - if (identical(_profile, binding)) _profile = null; - }); - _profile = binding; - } catch (_) { - binding.close(); - rethrow; - } - } - - /// Explicit discovery only when the prepared binding is missing or stale. - /// A still-valid binding is reused as-is, preserving the lease's recorded - /// Admin session evidence; discovery is never repeated implicitly. - Future prepareIfStale() async { - final binding = _profile; - if (binding != null && identical(binding.lease, _lease)) { - try { - binding.check(); - _cancellation.check(); - return; - } on StateError { - // Fall through to explicit discovery below. - } - } - await prepare(); - } - - _AdminProfileBinding get _prepared { - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare Admin in the current lease first'); - } - binding.check(); - _cancellation.check(); - if (lease.isExchanging) { - throw StateError('A card operation is already active'); - } - return binding; - } - /// Runs one Admin request. When this lease holds a still-valid recorded /// Admin authentication (see CardLease.recordAdminAuthentication) and the /// caller allows it, the request uses `Access::Existing`: no SELECT and no /// VERIFY are sent, and any supplied PIN stays unused. Otherwise the /// request SELECTs and verifies only the explicitly supplied PIN. - Future _execute( + Future executeAdminRequest( ProtocolOperation Function(ProtocolProfile profile, Uint8List? pin, bool existing) create, { String? pin, bool existingAllowed = true, bool mutation = false, }) async { - final binding = _prepared; - lastResponse = null; + final binding = preparedBinding; + lastStatusWord = null; lastProgress = null; final existing = existingAllowed && binding.lease.hasAdminAuthentication; if (!existing) { @@ -148,27 +54,23 @@ class AdminCardClient { try { final result = await executeAdminOperation( create(binding.profile, pinBytes, existing), - _transport, + transport, lease: binding.lease, - cancellation: _cancellation, + cancellation: cancellation, onProgress: (progress) => lastProgress = progress, ); binding.check(); - _cancellation.check(); - lastResponse = '9000'; + cancellation.check(); + lastStatusWord = '9000'; return result; } on ProtocolException catch (error) { - lastResponse = error.details.statusWord - ?.toRadixString(16) - .padLeft(4, '0') - .toUpperCase(); + lastStatusWord = formatStatusWord(error.details.statusWord); // A failed Existing request leaves the card-side session state // uncertain; later requests must re-establish evidence or use a PIN. if (existing) binding.lease.invalidateAdminAuthentication(); rethrow; } catch (_) { - binding.close(); - if (identical(_profile, binding)) _profile = null; + discardProfile(binding); rethrow; } finally { pinBytes?.fillRange(0, pinBytes.length, 0); @@ -182,17 +84,47 @@ class AdminCardClient { } if (progress?.reprobeRequired == true) { binding.lease.invalidateProfileEvidence(); - binding.close(); - if (identical(_profile, binding)) _profile = null; + discardProfile(binding); } } } +} - Future _read(AdminReadOperation kind, {String? pin}) => _execute( - (profile, pinBytes, existing) => - profile.adminRead(kind: kind, pin: pinBytes, existing: existing), - pin: pin, - ); +/// Each upstream Admin operation either SELECTs and explicitly authenticates +/// its own request, or reuses this lease's authenticated Admin session via +/// `Access::Existing`. That session evidence is recorded only by a successful +/// explicit [verifyPin] on the same lease and is bound to the lease's +/// selection/profile generations; a UI PIN cache is never authorization. +class AdminCardClient extends AdminSessionCardClient { + AdminCardClient({super.transport, super.lease}); + + /// Explicit minimal discovery, before any authentication. Repeat after a + /// profile-affecting write, including an uncertain write; never auto-reprobe. + Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + + /// Explicit discovery only when the prepared binding is missing or stale. + /// A still-valid binding is reused as-is, preserving the lease's recorded + /// Admin session evidence; discovery is never repeated implicitly. + Future prepareIfStale() async { + final binding = currentProfile; + if (binding != null && identical(binding.lease, lease)) { + try { + binding.check(); + cancellation.check(); + return; + } on StateError { + // Fall through to explicit discovery below. + } + } + await prepare(); + } + + Future _read(AdminReadOperation kind, {String? pin}) => + executeAdminRequest( + (profile, pinBytes, existing) => + profile.adminRead(kind: kind, pin: pinBytes, existing: existing), + pin: pin, + ); Future _action( AdminAction kind, { @@ -205,7 +137,7 @@ class AdminCardClient { }) async { final payload = Uint8List.fromList(data); try { - await _execute( + await executeAdminRequest( (profile, pinBytes, existing) => profile.adminAction( kind: kind, pin: pinBytes, @@ -234,8 +166,9 @@ class AdminCardClient { existingAllowed: false, mutation: false, ); - // _prepared revalidates the binding and lease identity before stamping. - _prepared.lease.recordAdminAuthentication(); + // preparedBinding revalidates the binding and lease identity before + // stamping. + preparedBinding.lease.recordAdminAuthentication(); return true; } on ProtocolException catch (error) { if (error.details.kind == 'AuthenticationFailed' || @@ -256,15 +189,15 @@ class AdminCardClient { } Future readFirmwareVersion() async => utf8.decode( - _prepared.profile.firmware() ?? + preparedBinding.profile.firmware() ?? (throw StateError('Missing firmware observation')), ); Future readModel() async => - _prepared.profile.model() ?? + preparedBinding.profile.model() ?? (throw StateError('Device did not report its model')); Future readSerial() async => hex .encode( - _prepared.profile.serial() ?? + preparedBinding.profile.serial() ?? (throw StateError('Device did not report its serial')), ) .toUpperCase(); @@ -330,7 +263,7 @@ class AdminCardClient { }) async { RangeError.checkValueInInterval(featureMask, 0, 0x3f, 'featureMask'); RangeError.checkValueInInterval(featureValues, 0, 0x3f, 'featureValues'); - await _execute( + await executeAdminRequest( (profile, pinBytes, existing) => profile.adminConfigure( pin: pinBytes, existing: existing, @@ -414,28 +347,3 @@ class AdminCardClient { await _action(AdminAction.writeSm2, pin: pin, data: data); } } - -class _AdminProfileBinding { - _AdminProfileBinding(this.profile, this.lease) - : generation = lease.profileGeneration; - final ProtocolProfile profile; - final CardLease lease; - final int generation; - bool _closed = false; - void check() { - lease.check(); - if (_closed || generation != lease.profileGeneration) { - throw StateError('Admin profile requires explicit discovery'); - } - } - - void close() { - if (_closed) return; - _closed = true; - try { - profile.close(); - } finally { - profile.dispose(); - } - } -} diff --git a/lib/helper/utils/card_client.dart b/lib/helper/utils/card_client.dart new file mode 100644 index 0000000..4e672c9 --- /dev/null +++ b/lib/helper/utils/card_client.dart @@ -0,0 +1,230 @@ +import 'dart:typed_data'; + +import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; + +/// Uppercase four-digit hex rendering of a card status word. +String? formatStatusWord(int? status) => + status?.toRadixString(16).padLeft(4, '0').toUpperCase(); + +/// Immutable profile evidence bound to one physical lease. The binding expires +/// on lease replacement and on profile invalidation; with [bindSelection] it +/// additionally expires on any later applet selection. The profile is +/// evidence, never an authentication token. +class ProfileBinding { + ProfileBinding(this.profile, this.lease, {bool bindSelection = false}) + : profileGeneration = lease.profileGeneration, + selectionGeneration = bindSelection ? lease.selectionGeneration : null; + + final ProtocolProfile profile; + final CardLease lease; + final int profileGeneration; + final int? selectionGeneration; + bool _closed = false; + + void check() { + lease.check(); + final selection = selectionGeneration; + if (_closed || + profileGeneration != lease.profileGeneration || + (selection != null && selection != lease.selectionGeneration)) { + throw StateError('Card profile requires explicit discovery'); + } + } + + void close() { + if (_closed) return; + _closed = true; + try { + profile.close(); + } finally { + profile.dispose(); + } + } +} + +/// Shared transport/lease plumbing for every card client: the production +/// transport reads SmartCard.currentLease, injected transports use a +/// caller-owned lease or [withSession], and cancellation is local evidence +/// that never races the executor's handle cleanup. +abstract class CardClientBase { + CardClientBase({ + ApduTransport transport = const SmartCardApduTransport(), + CardLease? lease, + }) : _transport = transport, + _injectedLease = lease { + if (lease != null && transport is SmartCardApduTransport) { + throw ArgumentError('Production transport uses SmartCard.currentLease'); + } + } + + final ApduTransport _transport; + final CardLease? _injectedLease; + final CardSessions _injectedSessions = CardSessions(); + + final CardCancellation cancellation = CardCancellation(); + + ApduTransport get transport => _transport; + + CardLease get lease => + _transport is SmartCardApduTransport + ? SmartCard.currentLease + : injectedCurrentLease ?? + (throw StateError('Injected transport requires withSession')); + + /// The caller-owned lease bound for an injected transport, if any. + CardLease? get injectedCurrentLease => + _injectedLease ?? _injectedSessions.current?.lease; + + /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. + Future withSession(Future Function() action) { + if (_transport is SmartCardApduTransport) { + throw StateError('Use SmartCard.process for the production transport'); + } + if (_injectedLease != null) { + throw StateError('Use the supplied lease owner'); + } + return _injectedSessions.run((session) async { + session.bind(_transport.transceive); + return action(); + }); + } + + /// Request cancellation; the active executor drains I/O and frees its handle. + void cancelPendingOperations() => cancellation.cancel(); +} + +/// Base for clients whose operations run against an explicitly prepared +/// [ProfileBinding]. Discovery is explicit, evidence is revalidated before and +/// after every operation, and failures never trigger a hidden re-probe. +abstract class ProfileCardClient extends CardClientBase { + ProfileCardClient({ + super.transport, + super.lease, + required bool bindSelection, + }) : _bindSelection = bindSelection; + + final bool _bindSelection; + ProfileBinding? _profile; + + /// Status word of the most recent prepared operation, uppercase hex; null + /// when no card response was observed. + String? lastStatusWord; + + ProfileBinding? get currentProfile => _profile; + + /// The prepared binding for this lease, revalidated before every operation. + ProfileBinding get preparedBinding { + final lease = this.lease; + final binding = _profile; + if (binding == null || !identical(binding.lease, lease)) { + throw StateError('Prepare the card profile in the current lease first'); + } + binding.check(); + cancellation.check(); + if (lease.isExchanging) { + throw StateError('A card operation is already active'); + } + return binding; + } + + /// Explicit minimal discovery, before any authentication. Repeat after a + /// profile-affecting write, including an uncertain write; never auto-reprobe. + Future prepareProfile( + ProtocolOperation Function() probe, { + bool rejectWhileExchanging = false, + }) async { + cancellation.check(); + final lease = this.lease; + if (rejectWhileExchanging && lease.isExchanging) { + throw StateError('Cannot replace a profile during an active operation'); + } + lease.willSelectApplet(); + _profile?.close(); + _profile = null; + final profile = await executeProfileProbe( + probe(), + transport, + lease: lease, + cancellation: cancellation, + ); + final binding = ProfileBinding( + profile, + lease, + bindSelection: _bindSelection, + ); + try { + lease.check(); + cancellation.check(); + lease.onClose(() { + binding.close(); + if (identical(_profile, binding)) _profile = null; + }); + _profile = binding; + } catch (_) { + binding.close(); + rethrow; + } + } + + /// Drop stale or uncertain profile evidence; never resumes or retries. + void discardProfile(ProfileBinding binding) { + binding.close(); + if (identical(_profile, binding)) _profile = null; + } + + /// Runs one prepared libcanokey operation and records [lastStatusWord]. + /// + /// - [selectApplet]: the operation SELECTs its applet first, expiring other + /// applets' selection evidence. + /// - [verifyProfileIdentity]: a profile replaced during the operation fails + /// instead of publishing a result. + /// - [recheckOnProtocolError]: revalidate binding/cancellation on a protocol + /// failure before recording the status word. + /// - [discardOnOtherError]: non-protocol failures discard the profile. + /// - [discardOnProtocolError]: decides per protocol failure whether the + /// profile evidence survives (e.g. expected credential rejections). + Future executePrepared( + ProtocolOperation Function(ProtocolProfile profile) create, { + bool selectApplet = false, + bool verifyProfileIdentity = false, + bool recheckOnProtocolError = true, + bool discardOnOtherError = false, + bool Function(ProtocolException error)? discardOnProtocolError, + }) async { + final binding = preparedBinding; + lastStatusWord = null; + if (selectApplet) binding.lease.willSelectApplet(); + try { + final data = await executeProtocolOperation( + create(binding.profile), + transport, + lease: binding.lease, + cancellation: cancellation, + ); + binding.check(); + cancellation.check(); + if (verifyProfileIdentity && !identical(_profile, binding)) { + throw StateError('Card profile replaced'); + } + lastStatusWord = '9000'; + return data; + } on ProtocolException catch (error) { + if (recheckOnProtocolError) { + binding.check(); + cancellation.check(); + } + lastStatusWord = formatStatusWord(error.details.statusWord); + if (discardOnProtocolError?.call(error) ?? false) { + discardProfile(binding); + } + rethrow; + } catch (_) { + if (discardOnOtherError) discardProfile(binding); + rethrow; + } + } +} diff --git a/lib/helper/utils/ctap_transmitter.dart b/lib/helper/utils/ctap_transmitter.dart deleted file mode 100644 index 8f27cf5..0000000 --- a/lib/helper/utils/ctap_transmitter.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; -import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; -import 'package:canokey_console/src/rust/api/protocol.dart'; -import 'package:fido2/fido2.dart'; - -/// libcanokey-backed CTAP transport. The facade owns the APDU envelope -/// (extended Lc or CLA chaining) and GET RESPONSE continuation; this layer -/// only selects the FIDO2 applet once per lease and forwards raw CTAP -/// messages. A non-zero CTAP status byte is data for the fido2 package, -/// never a transport error. -class CtapTransmitter extends CtapDevice { - CtapTransmitter({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } - - final ApduTransport _transport; - final CardLease? _injectedLease; - - /// The card's selected applet is global to the physical lease, so the - /// selection evidence is shared by every transmitter on that lease. - static CardLease? _selectedLease; - bool _selectedWithoutLease = false; - - CardLease? get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease; - - bool get _isSelected { - final lease = _lease; - return lease != null - ? identical(_selectedLease, lease) - : _selectedWithoutLease; - } - - /// Explicit FIDO2 selection through libcanokey; an absent applet surfaces - /// as UnsupportedDevice instead of a raw 6A82 status word. - Future selectApplication() async { - final lease = _lease; - lease?.willSelectApplet(); - await executeProtocolOperation( - ProtocolOperation.ctapSelectApplication(), - _transport, - lease: lease, - ); - if (lease != null) { - _selectedLease = lease; - } else { - _selectedWithoutLease = true; - } - } - - @override - Future>> transceive(List command) async { - if (!_isSelected) { - await selectApplication(); - } - final data = await executeProtocolOperation( - ProtocolOperation.ctapTransceiveSelected(message: command), - _transport, - lease: _lease, - ); - return CtapResponse(data.first, data.sublist(1)); - } -} diff --git a/lib/helper/utils/fido2_backend.dart b/lib/helper/utils/fido2_backend.dart deleted file mode 100644 index b04e959..0000000 --- a/lib/helper/utils/fido2_backend.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'fido2_backend_native.dart' - if (dart.library.js_interop) 'fido2_backend_web.dart'; diff --git a/lib/helper/utils/fido2_backend_native.dart b/lib/helper/utils/fido2_backend_native.dart deleted file mode 100644 index ae28eda..0000000 --- a/lib/helper/utils/fido2_backend_native.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:io'; - -import 'package:fido2/fido2.dart'; - -Future initializeFido2Backend() async { - if (RustCrypto.isInitialized) return; - const stem = 'rust_lib_canokey_console'; - final name = Platform.isWindows - ? '$stem.dll' - : Platform.isMacOS || Platform.isIOS - ? 'lib$stem.dylib' - : 'lib$stem.so'; - final directory = - Platform.environment['FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR'] ?? - 'rust/target/release'; - final developmentLibrary = File.fromUri( - Uri.directory(directory).resolve(name), - ); - final path = !Platform.isAndroid && developmentLibrary.existsSync() - ? developmentLibrary.absolute.path - : Platform.isMacOS || Platform.isIOS - ? '$stem.framework/$stem' - : name; - await RustCrypto.initialize( - libraryPath: Platform.environment['FIDO2_CRYPTO_LIBRARY'] ?? path, - ); -} diff --git a/lib/helper/utils/fido2_backend_web.dart b/lib/helper/utils/fido2_backend_web.dart deleted file mode 100644 index 75cdaf4..0000000 --- a/lib/helper/utils/fido2_backend_web.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:fido2/fido2.dart'; - -Future initializeFido2Backend() => - RustCrypto.initialize(wasmModuleUrl: 'fido2/fido2_crypto.js'); diff --git a/lib/helper/utils/ndef_card.dart b/lib/helper/utils/ndef_card.dart index 9dddb1f..2294543 100644 --- a/lib/helper/utils/ndef_card.dart +++ b/lib/helper/utils/ndef_card.dart @@ -1,9 +1,7 @@ import 'dart:typed_data'; -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/card_client.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; class NdefCardData { @@ -26,44 +24,8 @@ class NdefReadOnlyException implements Exception { /// NDEF applet and own CC/file selection, 240-byte chunking, continuation and /// the crash-safe write order (zero NLEN, message, real NLEN). A failed write /// is never replayed; transport errors propagate unchanged. -class NdefCardClient { - NdefCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } - - final ApduTransport _transport; - final CardLease? _injectedLease; - final CardSessions _injectedSessions = CardSessions(); - final CardCancellation _cancellation = CardCancellation(); - - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected NDEF transport requires withSession')); - - /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } - - /// Request cancellation; the active executor drains I/O and frees its handle. - void cancelPendingOperations() => _cancellation.cancel(); +class NdefCardClient extends CardClientBase { + NdefCardClient({super.transport, super.lease}); Future read() async { final capability = await _execute(ProtocolOperation.ndefReadCapability()); @@ -86,16 +48,16 @@ class NdefCardClient { /// Null only when the NDEF applet is absent (6A82 at SELECT). Future _execute(ProtocolOperation operation) async { - _cancellation.check(); - final lease = _lease; + cancellation.check(); + final lease = this.lease; // Every NDEF operation selects the applet before its target commands. lease.willSelectApplet(); try { return await executeProtocolOperation( operation, - _transport, + transport, lease: lease, - cancellation: _cancellation, + cancellation: cancellation, ); } on ProtocolException catch (error) { if (error.details.kind == 'UnsupportedDevice' && diff --git a/lib/helper/utils/oath_card.dart b/lib/helper/utils/oath_card.dart index 5337692..02e80bd 100644 --- a/lib/helper/utils/oath_card.dart +++ b/lib/helper/utils/oath_card.dart @@ -2,10 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; -import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/helper/utils/card_client.dart'; import 'package:canokey_console/models/oath.dart'; import 'package:canokey_console/src/rust/api/crypto.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; @@ -54,23 +51,8 @@ class OathCalculatedEntry { /// pagination, continuation and response validation. Every operation selects /// and, when an access key is supplied, validates within itself — no /// authentication state is carried between operations. -class OathCardClient { - OathCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } - - final ApduTransport _transport; - final CardLease? _injectedLease; - final CardSessions _injectedSessions = CardSessions(); - final CardCancellation _cancellation = CardCancellation(); - _OathProfileBinding? _profile; - String? lastStatusWord; +class OathCardClient extends ProfileCardClient { + OathCardClient({super.transport, super.lease}) : super(bindSelection: false); /// Fresh host challenge for each access proof; tests may pin a fixed value. Uint8List Function() challengeGenerator = _randomChallenge; @@ -80,29 +62,6 @@ class OathCardClient { return Uint8List.fromList(List.generate(8, (_) => random.nextInt(256))); } - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected OATH transport requires withSession')); - - /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } - - /// Request cancellation; the active executor drains I/O and frees its handle. - void cancelPendingOperations() => _cancellation.cancel(); - /// OATH password convention: PBKDF2-HMAC-SHA1, 1000 iterations, the SELECT /// handle as salt, sixteen output bytes. static Uint8List deriveKey(String code, List salt) => @@ -110,77 +69,18 @@ class OathCardClient { /// Explicit minimal discovery, before any authentication. Repeat after a /// profile-affecting write, including an uncertain write; never auto-reprobe. - Future prepare() async { - _cancellation.check(); - final lease = _lease; - lease.willSelectApplet(); - _profile?.close(); - _profile = null; - final profile = await executeProfileProbe( - ProtocolOperation.probeAdmin(), - _transport, - lease: lease, - cancellation: _cancellation, - ); - final binding = _OathProfileBinding(profile, lease); - try { - lease.check(); - _cancellation.check(); - lease.onClose(() { - binding.close(); - if (identical(_profile, binding)) _profile = null; - }); - _profile = binding; - } catch (_) { - binding.close(); - rethrow; - } - } - - _OathProfileBinding get _prepared { - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare OATH in the current lease first'); - } - binding.check(); - _cancellation.check(); - if (lease.isExchanging) { - throw StateError('A card operation is already active'); - } - return binding; - } + Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + /// All OATH operations SELECT the applet before their target command. A + /// non-protocol failure discards the profile evidence. Future _execute( ProtocolOperation Function(ProtocolProfile) create, - ) async { - final binding = _prepared; - lastStatusWord = null; - // All OATH operations SELECT the applet before their target command. - binding.lease.willSelectApplet(); - try { - final data = await executeProtocolOperation( - create(binding.profile), - _transport, - lease: binding.lease, - cancellation: _cancellation, - ); - binding.check(); - _cancellation.check(); - lastStatusWord = '9000'; - return data; - } on ProtocolException catch (error) { - lastStatusWord = error.details.statusWord - ?.toRadixString(16) - .padLeft(4, '0') - .toUpperCase(); - rethrow; - } catch (_) { - binding.close(); - if (identical(_profile, binding)) _profile = null; - rethrow; - } - } + ) => executePrepared( + create, + selectApplet: true, + recheckOnProtocolError: false, + discardOnOtherError: true, + ); (Uint8List?, Uint8List?) _accessParts(Uint8List? key) => key == null ? (null, null) : (key, challengeGenerator()); @@ -406,29 +306,3 @@ class OathCardClient { return result; } } - -class _OathProfileBinding { - _OathProfileBinding(this.profile, this.lease) - : generation = lease.profileGeneration; - final ProtocolProfile profile; - final CardLease lease; - final int generation; - bool _closed = false; - - void check() { - lease.check(); - if (_closed || generation != lease.profileGeneration) { - throw StateError('OATH profile requires explicit discovery'); - } - } - - void close() { - if (_closed) return; - _closed = true; - try { - profile.close(); - } finally { - profile.dispose(); - } - } -} diff --git a/lib/helper/utils/openpgp_card.dart b/lib/helper/utils/openpgp_card.dart index 8e9085f..2a2716c 100644 --- a/lib/helper/utils/openpgp_card.dart +++ b/lib/helper/utils/openpgp_card.dart @@ -2,10 +2,8 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:canokey_console/helper/tlv.dart'; -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/card_client.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/openpgp.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; @@ -13,118 +11,21 @@ import 'package:convert/convert.dart'; /// libcanokey owns every OpenPGP operation: card-info reads, PW1/PW3 /// credentials and all administrative writes. Each upstream operation SELECTs /// OpenPGP and explicitly verifies its own password; the prepared profile is -/// immutable firmware evidence, never an authorization token. -class OpenPgpCardClient { - OpenPgpCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } - - final ApduTransport _transport; - final CardLease? _injectedLease; - final CardSessions _injectedSessions = CardSessions(); - final CardCancellation _cancellation = CardCancellation(); - _OpenPgpProfileBinding? _profile; - String? lastStatusWord; - - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected OpenPGP transport requires withSession')); - - /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } - - void cancelPendingOperations() => _cancellation.cancel(); +/// immutable firmware evidence, never an authorization token. OpenPGP +/// operations self-SELECT, so applet switches do not invalidate the profile; +/// only profile evidence and lease generations matter. +class OpenPgpCardClient extends ProfileCardClient { + OpenPgpCardClient({super.transport, super.lease}) + : super(bindSelection: false); /// Explicit minimal discovery before any OpenPGP operation. The profile only /// carries firmware observations; upstream operations SELECT OpenPGP themselves. - Future prepare() async { - _cancellation.check(); - final lease = _lease; - lease.willSelectApplet(); - _profile?.close(); - _profile = null; - final profile = await executeProfileProbe( - ProtocolOperation.probeAdmin(), - _transport, - lease: lease, - cancellation: _cancellation, - ); - final binding = _OpenPgpProfileBinding(profile, lease); - try { - lease.check(); - _cancellation.check(); - lease.onClose(() { - binding.close(); - if (identical(_profile, binding)) _profile = null; - }); - _profile = binding; - } catch (_) { - binding.close(); - rethrow; - } - } - - _OpenPgpProfileBinding get _prepared { - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare OpenPGP in the current lease first'); - } - binding.check(); - _cancellation.check(); - if (lease.isExchanging) { - throw StateError('A card operation is already active'); - } - return binding; - } + Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + /// Every upstream OpenPGP operation SELECTs its applet first. Future _execute( ProtocolOperation Function(ProtocolProfile) create, - ) async { - final binding = _prepared; - lastStatusWord = null; - // Every upstream OpenPGP operation SELECTs its applet first. - binding.lease.willSelectApplet(); - try { - final data = await executeProtocolOperation( - create(binding.profile), - _transport, - lease: binding.lease, - cancellation: _cancellation, - ); - binding.check(); - _cancellation.check(); - lastStatusWord = '9000'; - return data; - } on ProtocolException catch (error) { - binding.check(); - _cancellation.check(); - lastStatusWord = error.details.statusWord - ?.toRadixString(16) - .padLeft(4, '0') - .toUpperCase(); - rethrow; - } - } + ) => executePrepared(create, selectApplet: true); Future readCardInfo() async { final applicationData = await _readDataObject(0x6E); @@ -539,30 +440,3 @@ class OpenPgpCardClient { return DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true); } } - -/// Firmware-evidence binding; OpenPGP operations self-SELECT, so applet -/// switches do not invalidate it. Only profile evidence and lease matter. -class _OpenPgpProfileBinding { - _OpenPgpProfileBinding(this.profile, this.lease) - : generation = lease.profileGeneration; - final ProtocolProfile profile; - final CardLease lease; - final int generation; - bool _closed = false; - void check() { - lease.check(); - if (_closed || generation != lease.profileGeneration) { - throw StateError('OpenPGP profile requires explicit discovery'); - } - } - - void close() { - if (_closed) return; - _closed = true; - try { - profile.close(); - } finally { - profile.dispose(); - } - } -} diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index 1e82199..4924331 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -1,10 +1,7 @@ -import 'dart:convert'; import 'dart:typed_data'; -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; +import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/models/pass.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:convert/convert.dart'; @@ -13,155 +10,18 @@ import 'package:convert/convert.dart'; /// SELECTs Admin (a protected write verifying its own explicit PIN) or reuses /// the lease's recorded Admin authentication via `Access::Existing`. A UI PIN /// cache is never authorization evidence. -class PassCardClient { - PassCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, - }) : _transport = transport, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } - - final ApduTransport _transport; - final CardLease? _injectedLease; - final CardSessions _injectedSessions = CardSessions(); - final CardCancellation _cancellation = CardCancellation(); - _AdminProfileBinding? _profile; - String? lastResponse; - AdminProgress? lastProgress; - - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected Pass transport requires withSession')); - - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } - - void cancelPendingOperations() => _cancellation.cancel(); +class PassCardClient extends AdminSessionCardClient { + PassCardClient({super.transport, super.lease}); /// Explicit minimal Admin discovery, before any slot operation. Repeat after /// a slot write, including an uncertain write; never auto-reprobe. - Future prepare() async { - _cancellation.check(); - final lease = _lease; - lease.willSelectApplet(); - _profile?.close(); - _profile = null; - final profile = await executeProfileProbe( - ProtocolOperation.probeAdmin(), - _transport, - lease: lease, - cancellation: _cancellation, - ); - final binding = _AdminProfileBinding(profile, lease); - try { - lease.check(); - _cancellation.check(); - lease.onClose(() { - binding.close(); - if (identical(_profile, binding)) _profile = null; - }); - _profile = binding; - } catch (_) { - binding.close(); - rethrow; - } - } - - _AdminProfileBinding get _prepared { - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare Admin in the current lease first'); - } - binding.check(); - _cancellation.check(); - if (lease.isExchanging) { - throw StateError('A card operation is already active'); - } - return binding; - } - - /// Runs one Admin request. A still-valid recorded Admin authentication on - /// this lease selects `Access::Existing` (no SELECT/VERIFY, no PIN); any - /// other request SELECTs and verifies only its explicitly supplied PIN. - Future _execute( - ProtocolOperation Function(ProtocolProfile profile, Uint8List? pin, bool existing) - create, { - String? pin, - bool mutation = false, - }) async { - final binding = _prepared; - lastResponse = null; - lastProgress = null; - final existing = binding.lease.hasAdminAuthentication; - if (!existing) { - // Selecting factories invalidate any selected PIV context before - // starting, even though immutable Admin profile evidence can be reused. - binding.lease.willSelectApplet(); - } - final pinBytes = existing || pin == null - ? null - : Uint8List.fromList(utf8.encode(pin)); - try { - final result = await executeAdminOperation( - create(binding.profile, pinBytes, existing), - _transport, - lease: binding.lease, - cancellation: _cancellation, - onProgress: (progress) => lastProgress = progress, - ); - binding.check(); - _cancellation.check(); - lastResponse = '9000'; - return result; - } on ProtocolException catch (error) { - lastResponse = error.details.statusWord - ?.toRadixString(16) - .padLeft(4, '0') - .toUpperCase(); - // A failed Existing request leaves the card-side session uncertain. - if (existing) binding.lease.invalidateAdminAuthentication(); - rethrow; - } catch (_) { - binding.close(); - if (identical(_profile, binding)) _profile = null; - rethrow; - } finally { - pinBytes?.fillRange(0, pinBytes.length, 0); - final progress = lastProgress; - if (mutation && - progress != null && - (progress.confirmedWrites > 0 || progress.reprobeRequired)) { - binding.lease.invalidateAdminAuthentication(); - } - if (progress?.reprobeRequired == true) { - binding.lease.invalidateProfileEvidence(); - binding.close(); - if (identical(_profile, binding)) _profile = null; - } - } - } + Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); /// Reads both slots. Firmware gates INS 43 behind Admin authentication, so /// callers pass the lease's verified PIN; a still-valid recorded Admin /// authentication takes precedence and the PIN is not sent at all. Future> readSlots({String? pin}) async { - final result = await _execute( + final result = await executeAdminRequest( (profile, pinBytes, existing) => profile.adminPassSlots(pin: pinBytes, existing: existing), pin: pin, @@ -229,7 +89,7 @@ class PassCardClient { secret = Uint8List.fromList(key); } try { - await _execute( + await executeAdminRequest( (profile, pinBytes, existing) => profile.adminSetPassSlot( slot: index - 1, kind: kind, @@ -253,28 +113,3 @@ class PassCardClient { } } } - -class _AdminProfileBinding { - _AdminProfileBinding(this.profile, this.lease) - : generation = lease.profileGeneration; - final ProtocolProfile profile; - final CardLease lease; - final int generation; - bool _closed = false; - void check() { - lease.check(); - if (_closed || generation != lease.profileGeneration) { - throw StateError('Admin profile requires explicit discovery'); - } - } - - void close() { - if (_closed) return; - _closed = true; - try { - profile.close(); - } finally { - profile.dispose(); - } - } -} diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index ba0e654..34bf065 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -2,129 +2,70 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; -import 'package:canokey_console/helper/utils/smartcard.dart'; +import 'package:canokey_console/helper/utils/card_client.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; import 'package:canokey_console/src/rust/api/protocol.dart'; import 'package:canokey_console/models/piv.dart'; import 'package:convert/convert.dart'; -class PivCardClient { +class PivCardClient extends ProfileCardClient { PivCardClient({ - ApduTransport transport = const SmartCardApduTransport(), - CardLease? lease, + super.transport, + super.lease, PivReadExecutor? readExecutor, PivCertificateExecutor? certificateExecutor, Future Function()? prepareExecutor, - }) : _transport = transport, - _readExecutor = readExecutor, + }) : _readExecutor = readExecutor, _certificateExecutor = certificateExecutor, _prepareExecutor = prepareExecutor, - _injectedLease = lease { - if (lease != null && transport is SmartCardApduTransport) { - throw ArgumentError('Production transport uses SmartCard.currentLease'); - } - } + super(bindSelection: true); - final ApduTransport _transport; - final CardLease? _injectedLease; final PivReadExecutor? _readExecutor; final PivCertificateExecutor? _certificateExecutor; final Future Function()? _prepareExecutor; - final CardSessions _injectedSessions = CardSessions(); - _PivProfileBinding? _profile; - - CardLease get _lease => _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? - _injectedSessions.current?.lease ?? - (throw StateError('Injected transport requires withSession')); - - /// For caller-owned transports (tests/USB-IP). Production uses SmartCard.process. - Future withSession(Future Function() action) { - if (_transport is SmartCardApduTransport) { - throw StateError('Use SmartCard.process for the production transport'); - } - if (_injectedLease != null) { - throw StateError('Use the supplied lease owner'); - } - return _injectedSessions.run((session) async { - session.bind(_transport.transceive); - return action(); - }); - } /// Explicit discovery/selection before authentication. The lease owns the /// immutable observations; later metadata reads never probe or SELECT. Future prepare() async { - _cancellation.check(); + cancellation.check(); final override = _prepareExecutor; if (override != null) return override(); - final lease = _lease; - if (lease.isExchanging) { - throw StateError('Cannot replace a profile during an active operation'); - } - lease.willSelectApplet(); - _profile?.close(); - _profile = null; - final profile = await executeProfileProbe( - ProtocolOperation.probePiv(), - _transport, - lease: lease, - cancellation: _cancellation, + await prepareProfile( + ProtocolOperation.probePiv, + rejectWhileExchanging: true, ); - final binding = _PivProfileBinding(profile, lease); - try { - lease.check(); - _cancellation.check(); - lease.onClose(() { - binding.close(); - if (identical(_profile, binding)) _profile = null; - }); - _profile = binding; - } catch (_) { - binding.close(); - rethrow; - } } - final CardCancellation _cancellation = CardCancellation(); - - /// Request cancellation; the active executor drains I/O and frees its handle. - void cancelPendingOperations() => _cancellation.cancel(); - Future _read(PivReadOperation kind) async { - _cancellation.check(); - final binding = kind == PivReadOperation.select ? null : _profile; + cancellation.check(); + final binding = kind == PivReadOperation.select ? null : currentProfile; binding?.check(); final executor = _readExecutor; if (executor != null) return executor(kind); final data = await executeProtocolOperation( ProtocolOperation.pivRead(kind: kind), - _transport, - lease: _transport is SmartCardApduTransport + transport, + lease: transport is SmartCardApduTransport ? null - : _injectedLease ?? _injectedSessions.current?.lease, - cancellation: _cancellation, + : injectedCurrentLease, + cancellation: cancellation, ); binding?.check(); - _cancellation.check(); + cancellation.check(); return data; } - String? lastStatusWord; - Future select() async { - final lease = _transport is SmartCardApduTransport - ? SmartCard.currentLease - : _injectedLease ?? _injectedSessions.current?.lease; + final lease = transport is SmartCardApduTransport + ? this.lease + : injectedCurrentLease; lease?.willSelectApplet(); - final binding = _profile; + final binding = currentProfile; if (binding != null) { if (binding.lease.isExchanging) { throw StateError('Cannot SELECT during an active operation'); } - _discardProfile(binding); + discardProfile(binding); } await _read(PivReadOperation.select); } @@ -181,9 +122,9 @@ class PivCardClient { /// A successful write replaces the observed algorithm IDs; upstream marks it /// ReprobeRequired, so the prepared profile is always discarded afterwards. Future setAlgorithmConfig(Uint8List raw) async { - final binding = _prepared; + final binding = preparedBinding; await _executePrepared((profile) => profile.pivSetAlgorithmConfig(raw: raw)); - _discardProfile(binding); + discardProfile(binding); } Future setContainerName(int slot, String name) async { @@ -342,71 +283,29 @@ class PivCardClient { )); } - _PivProfileBinding get _prepared { - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare PIV in this lease before this operation'); - } - binding.check(); - _cancellation.check(); - if (lease.isExchanging) { - throw StateError('A card operation is already active'); - } - return binding; - } - - void _discardProfile(_PivProfileBinding binding) { - binding.close(); - if (identical(_profile, binding)) _profile = null; - } - /// Use the serial observed before authentication; never switch applets here. Future readSerial() async { - final serial = _prepared.profile.serial(); + final serial = preparedBinding.profile.serial(); if (serial == null) throw StateError('Device did not report a serial'); return hex.encode(serial).toUpperCase(); } + /// Runs one prepared PIV operation. A rejected credential leaves immutable + /// device observations valid; an uncertain exchange/acknowledgment discards + /// the profile and cannot be used for dependent work. Future _executePrepared( ProtocolOperation Function(ProtocolProfile) create, { bool allowMissing = false, - }) async { - final binding = _prepared; - lastStatusWord = null; - try { - final data = await executeProtocolOperation( - create(binding.profile), - _transport, - lease: binding.lease, - cancellation: _cancellation, - ); - binding.check(); - _cancellation.check(); - if (!identical(_profile, binding)) { - throw StateError('PIV profile replaced'); - } - lastStatusWord = '9000'; - return data; - } on ProtocolException catch (error) { - binding.check(); - _cancellation.check(); - final status = error.details.statusWord; - lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); - // A rejected credential leaves immutable device observations valid. An - // uncertain exchange/acknowledgment cannot be used for dependent work. - if (error.exchangeAttempted && - error.details.kind != 'AuthenticationFailed' && - error.details.kind != 'PinBlocked' && - !(allowMissing && error.details.kind == 'NotFound')) { - _discardProfile(binding); - } - rethrow; - } catch (_) { - _discardProfile(binding); - rethrow; - } - } + }) => executePrepared( + create, + verifyProfileIdentity: true, + discardOnOtherError: true, + discardOnProtocolError: (error) => + error.exchangeAttempted && + error.details.kind != 'AuthenticationFailed' && + error.details.kind != 'PinBlocked' && + !(allowMissing && error.details.kind == 'NotFound'), + ); /// Selected-only empty VERIFY; 9000 does not imply a known retry count. Future readPinRetries() async { @@ -546,30 +445,13 @@ class PivCardClient { PivAlgorithmExtensionConfig? algorithmExtensionConfig, }) async { RangeError.checkValueInInterval(slot, 0, 0xff, 'slot'); - final lease = _lease; - final binding = _profile; - if (binding == null || !identical(binding.lease, lease)) { - throw StateError('Prepare PIV in this lease before reading metadata'); - } - binding.check(); - _cancellation.check(); - lastStatusWord = null; + final binding = preparedBinding; Uint8List data; try { - data = await executeProtocolOperation( - binding.profile.pivMetadata(reference: slot), - _transport, - lease: lease, - cancellation: _cancellation, + data = await executePrepared( + (profile) => profile.pivMetadata(reference: slot), ); - binding.check(); - _cancellation.check(); - lastStatusWord = '9000'; } on ProtocolException catch (error) { - binding.check(); - _cancellation.check(); - final status = error.details.statusWord; - lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); if (error.details.kind == 'NotFound') return null; rethrow; } @@ -594,29 +476,29 @@ class PivCardClient { Future readCertificate(int objectId) async { RangeError.checkValueInInterval(objectId, 0, 0xff, 'objectId'); final executor = _certificateExecutor; - final binding = executor == null ? _prepared : null; + final binding = executor == null ? preparedBinding : null; lastStatusWord = null; try { - _cancellation.check(); + cancellation.check(); final certificate = executor != null ? await executor(objectId) : await executeProtocolOperation( binding!.profile.pivCertificate(objectId: objectId), - _transport, + transport, lease: binding.lease, - cancellation: _cancellation, + cancellation: cancellation, ); - _cancellation.check(); + cancellation.check(); binding?.check(); lastStatusWord = '9000'; return certificate; } on ProtocolException catch (error) { binding?.check(); - _cancellation.check(); - final status = error.details.statusWord; - lastStatusWord = status?.toRadixString(16).padLeft(4, '0').toUpperCase(); + cancellation.check(); + lastStatusWord = formatStatusWord(error.details.statusWord); if (error.details.phase == 'Command' && - (status == 0x6a82 || status == 0x6a88)) { + (error.details.statusWord == 0x6a82 || + error.details.statusWord == 0x6a88)) { return null; } rethrow; @@ -658,35 +540,3 @@ class PivCardClient { } } } - -class _PivProfileBinding { - _PivProfileBinding(this.profile, this.lease) - : selectionGeneration = lease.selectionGeneration, - profileGeneration = lease.profileGeneration; - final ProtocolProfile profile; - final CardLease lease; - bool _closed = false; - final int selectionGeneration; - final int profileGeneration; - - void check() { - lease.check(); - if (_closed || - selectionGeneration != lease.selectionGeneration || - profileGeneration != lease.profileGeneration) { - throw StateError( - 'PIV selection or device observations are no longer current', - ); - } - } - - void close() { - if (_closed) return; - _closed = true; - try { - profile.close(); - } finally { - profile.dispose(); - } - } -} diff --git a/lib/helper/utils/protocol_operation.dart b/lib/helper/utils/protocol_operation.dart index f59fd3d..21270e5 100644 --- a/lib/helper/utils/protocol_operation.dart +++ b/lib/helper/utils/protocol_operation.dart @@ -51,6 +51,38 @@ Future executeProfileProbe( step.profile ?? (throw StateError('Expected profile result')), ); +/// Runs one ClientPIN key agreement and returns the session handle. The +/// caller owns the handle and closes it after its dependent operations. +Future executeCtapPinSession( + ProtocolOperation operation, + ApduTransport transport, { + CardLease? lease, + CardCancellation? cancellation, +}) => _execute( + operation, + transport, + lease: lease, + cancellation: cancellation, + result: (step) => + step.pinSession ?? (throw StateError('Expected CTAP PIN session')), +); + +/// Runs one ClientPIN token request and returns the token handle. The caller +/// owns the handle and closes it after use; tokens are never cached. +Future executeCtapPinToken( + ProtocolOperation operation, + ApduTransport transport, { + CardLease? lease, + CardCancellation? cancellation, +}) => _execute( + operation, + transport, + lease: lease, + cancellation: cancellation, + result: (step) => + step.pinToken ?? (throw StateError('Expected CTAP PIN token')), +); + Future executeAdminOperation( ProtocolOperation operation, ApduTransport transport, { @@ -96,7 +128,11 @@ Future _execute( if (step.error case final error?) { throw ProtocolException(error, exchangeAttempted: exchangeAttempted); } - if (step.data != null || step.profile != null || step.admin != null) { + if (step.data != null || + step.profile != null || + step.admin != null || + step.pinSession != null || + step.pinToken != null) { try { check(); return result(step); @@ -111,6 +147,22 @@ Future _execute( profile.dispose(); } } + final pinSession = step.pinSession; + if (pinSession != null) { + try { + pinSession.close(); + } finally { + pinSession.dispose(); + } + } + final pinToken = step.pinToken; + if (pinToken != null) { + try { + pinToken.close(); + } finally { + pinToken.dispose(); + } + } rethrow; } } diff --git a/lib/helper/utils/webauthn_card.dart b/lib/helper/utils/webauthn_card.dart new file mode 100644 index 0000000..9e461d5 --- /dev/null +++ b/lib/helper/utils/webauthn_card.dart @@ -0,0 +1,375 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:canokey_console/helper/utils/card_client.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/src/rust/api/protocol.dart'; + +/// Parsed authenticatorGetInfo fields, decoded in Rust and self-delimiting +/// here: `credMgmt | clientPin | forcePinChange` tri-state bytes +/// (0 = absent, 1 = false, 2 = true), a minPinLength flag byte (0 = absent; +/// 1 = present with a big-endian u64), then `count | pinUvAuthProtocol +/// version bytes`. +class WebAuthnInfo { + const WebAuthnInfo({ + required this.credMgmt, + required this.clientPin, + required this.forcePinChange, + required this.minPinLength, + required this.pinUvAuthProtocols, + }); + + final bool? credMgmt; + final bool? clientPin; + final bool? forcePinChange; + final int? minPinLength; + final List pinUvAuthProtocols; + + /// The ClientPIN protocol to speak: prefer V2 when advertised, like the + /// former fido2 package; fall back to V1 (the CTAP2 default). + int get pinProtocol => pinUvAuthProtocols.contains(2) ? 2 : 1; + + factory WebAuthnInfo.decode(Uint8List data) { + bool? tristate(int value) => switch (value) { + 0 => null, + 1 => false, + 2 => true, + _ => throw const FormatException('Invalid WebAuthn option tri-state'), + }; + + var offset = 0; + int take() { + if (offset >= data.length) { + throw const FormatException('Truncated WebAuthn getInfo data'); + } + return data[offset++]; + } + + final credMgmt = tristate(take()); + final clientPin = tristate(take()); + final forcePinChange = tristate(take()); + int? minPinLength; + final minPinLengthFlag = take(); + if (minPinLengthFlag == 1) { + if (data.length - offset < 8) { + throw const FormatException('Truncated WebAuthn minPinLength'); + } + minPinLength = ByteData.sublistView(data).getUint64(offset); + offset += 8; + } else if (minPinLengthFlag != 0) { + throw const FormatException('Invalid WebAuthn minPinLength flag'); + } + final protocolCount = take(); + if (data.length - offset != protocolCount) { + throw const FormatException('Truncated WebAuthn protocol list'); + } + return WebAuthnInfo( + credMgmt: credMgmt, + clientPin: clientPin, + forcePinChange: forcePinChange, + minPinLength: minPinLength, + pinUvAuthProtocols: List.unmodifiable(data.sublist(offset)), + ); + } +} + +/// One relying party with resident credentials. +class WebAuthnRp { + const WebAuthnRp({required this.id, required this.name, required this.idHash}); + + final String id; + final String? name; + final Uint8List idHash; +} + +/// One resident credential. In metadataOnly mode [publicKey] is absent and +/// [coseAlgorithm] carries the raw CanoKey extension value. +class WebAuthnCredential { + const WebAuthnCredential({ + required this.credentialId, + required this.userId, + required this.userName, + required this.userDisplayName, + required this.credProtect, + required this.coseAlgorithm, + required this.publicKey, + }); + + final Uint8List credentialId; + final Uint8List? userId; + final String? userName; + final String? userDisplayName; + final int? credProtect; + final int coseAlgorithm; + final Uint8List? publicKey; +} + +/// A ClientPIN key-agreement session. The shared secret lives only in the +/// zeroizing Rust handle; [close] is idempotent local cleanup. Obtain a +/// fresh session per use case instead of caching this wrapper. +class WebAuthnPinSession { + WebAuthnPinSession._(this._client, this._handle); + + final WebAuthnCardClient _client; + CtapPinSession? _handle; + + CtapPinSession get _session => + _handle ?? (throw StateError('WebAuthn PIN session is closed')); + + int get protocolVersion => _session.protocolVersion(); + + Future setPin(String newPin) async => + _client._execute(_session.setPin(newPin: utf8.encode(newPin))); + + Future changePin(String oldPin, String newPin) async => + _client._execute( + _session.changePin( + oldPin: utf8.encode(oldPin), newPin: utf8.encode(newPin)), + ); + + /// Mint a pinUvAuthToken (credentialManagement permission 0x04). The token + /// outlives this session; close the session once the token exists. + Future getPinToken( + String pin, { + int permissions = WebAuthnCardClient.permissionCredentialManagement, + String? rpId, + }) async { + final token = await _client._executeToken( + _session.getPinTokenWithPermissions( + pin: utf8.encode(pin), + permissions: permissions, + rpId: rpId, + ), + ); + return WebAuthnPinToken._(_client, token); + } + + void close() { + final handle = _handle; + _handle = null; + if (handle != null) { + try { + handle.close(); + } finally { + handle.dispose(); + } + } + } +} + +/// A decrypted pinUvAuthToken held only in zeroizing Rust memory. Per CTAP +/// 2.1 tokens are ceremony-scoped: never cache this wrapper across use cases. +class WebAuthnPinToken { + WebAuthnPinToken._(this._client, this._handle); + + final WebAuthnCardClient _client; + CtapPinToken? _handle; + + CtapPinToken get _token => + _handle ?? (throw StateError('WebAuthn PIN token is closed')); + + int get protocolVersion => _token.protocolVersion(); + + Future> enumerateRps() async => + _decodeRps(await _client._execute(_token.enumerateRps())); + + Future> enumerateCredentials( + Uint8List rpIdHash, { + bool metadataOnly = true, + }) async => _decodeCredentials( + await _client._execute( + _token.enumerateCredentials( + rpIdHash: rpIdHash, + metadataOnly: metadataOnly, + ), + ), + ); + + /// Permanently delete one resident credential. An unknown ID surfaces as a + /// NotFound ProtocolException with the raw CTAP status byte; never retried. + Future deleteCredential(List credentialId) async => + _client._execute(_token.deleteCredential(credentialId: credentialId)); + + void close() { + final handle = _handle; + _handle = null; + if (handle != null) { + try { + handle.close(); + } finally { + handle.dispose(); + } + } + } + + /// `id_len | id | name_len (0xff = absent) | name | rp_id_hash[32]` per RP. + static List _decodeRps(Uint8List data) { + final rps = []; + var offset = 0; + int take() { + if (offset >= data.length) { + throw const FormatException('Truncated WebAuthn RP entry'); + } + return data[offset++]; + } + + Uint8List takeBytes(int length, String name) { + if (length < 0 || data.length - offset < length) { + throw FormatException('Truncated WebAuthn RP $name'); + } + final bytes = Uint8List.sublistView(data, offset, offset + length); + offset += length; + return bytes; + } + + while (offset < data.length) { + final id = utf8.decode(takeBytes(take(), 'id')); + final nameLength = take(); + final name = nameLength == 0xff + ? null + : utf8.decode(takeBytes(nameLength, 'name')); + rps.add(WebAuthnRp(id: id, name: name, idHash: takeBytes(32, 'hash'))); + } + return rps; + } + + /// `cred_id_len u16BE | cred_id | user_flag | [user_id_len | user_id | + /// name_len (0xff absent) | name | display_len (0xff absent) | display] | + /// cred_protect (0xff absent) | key_form` per credential; key_form 0x01 is + /// metadataOnly with a raw i64 COSE algorithm, 0x02 carries the resolved + /// i64 algorithm plus `key_len u16BE | canonical CBOR key` pass-through. + static List _decodeCredentials(Uint8List data) { + final credentials = []; + var offset = 0; + int take() { + if (offset >= data.length) { + throw const FormatException('Truncated WebAuthn credential entry'); + } + return data[offset++]; + } + + Uint8List takeBytes(int length, String name) { + if (length < 0 || data.length - offset < length) { + throw FormatException('Truncated WebAuthn credential $name'); + } + final bytes = Uint8List.sublistView(data, offset, offset + length); + offset += length; + return bytes; + } + + String? takeText(String name) { + final length = take(); + return length == 0xff ? null : utf8.decode(takeBytes(length, name)); + } + + while (offset < data.length) { + if (data.length - offset < 2) { + throw const FormatException('Truncated WebAuthn credential ID length'); + } + final idLength = (take() << 8) | take(); + final credentialId = takeBytes(idLength, 'ID'); + Uint8List? userId; + String? userName; + String? userDisplayName; + if (take() case 1) { + userId = takeBytes(take(), 'user ID'); + userName = takeText('user name'); + userDisplayName = takeText('user display name'); + } + final credProtectByte = take(); + final credProtect = credProtectByte == 0xff ? null : credProtectByte; + final int coseAlgorithm; + final Uint8List? publicKey; + switch (take()) { + case 0x01: + if (data.length - offset < 8) { + throw const FormatException('Truncated WebAuthn COSE algorithm'); + } + coseAlgorithm = ByteData.sublistView(data).getInt64(offset); + offset += 8; + publicKey = null; + case 0x02: + if (data.length - offset < 8) { + throw const FormatException('Truncated WebAuthn COSE algorithm'); + } + coseAlgorithm = ByteData.sublistView(data).getInt64(offset); + offset += 8; + publicKey = takeBytes((take() << 8) | take(), 'public key'); + default: + throw const FormatException('Unknown WebAuthn credential key form'); + } + credentials.add( + WebAuthnCredential( + credentialId: credentialId, + userId: userId, + userName: userName, + userDisplayName: userDisplayName, + credProtect: credProtect, + coseAlgorithm: coseAlgorithm, + publicKey: publicKey, + ), + ); + } + return credentials; + } +} + +/// libcanokey-backed WebAuthn (CTAP2) client. Every operation is profile-free +/// and SELECTs the FIDO2 applet itself, so consecutive operations in one use +/// case never assume a residual selection. CTAP-level failures surface as +/// [ProtocolException] whose `details.statusWord` carries the raw CTAP status +/// byte (for example 0x31 PIN_INVALID), not an ISO 7816 status word. +class WebAuthnCardClient extends CardClientBase { + WebAuthnCardClient({super.transport, super.lease}); + + static const int permissionCredentialManagement = 0x04; + + Future getInfo() async => + WebAuthnInfo.decode(await _execute(ProtocolOperation.ctapGetInfo())); + + /// Start a ClientPIN session with the protocol preferred by [info]. The + /// caller closes the returned session after its dependent operations. + Future beginPinSession(WebAuthnInfo info) async { + final session = await _executeSession( + ProtocolOperation.ctapBeginPinSession(protocol: info.pinProtocol), + ); + return WebAuthnPinSession._(this, session); + } + + Future _execute(ProtocolOperation operation) { + cancellation.check(); + final lease = this.lease; + // Every CTAP2 client operation selects the FIDO2 applet first. + lease.willSelectApplet(); + return executeProtocolOperation( + operation, + transport, + lease: lease, + cancellation: cancellation, + ); + } + + Future _executeSession(ProtocolOperation operation) { + cancellation.check(); + final lease = this.lease; + lease.willSelectApplet(); + return executeCtapPinSession( + operation, + transport, + lease: lease, + cancellation: cancellation, + ); + } + + Future _executeToken(ProtocolOperation operation) { + cancellation.check(); + final lease = this.lease; + lease.willSelectApplet(); + return executeCtapPinToken( + operation, + transport, + lease: lease, + cancellation: cancellation, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 9fc350e..c8a2162 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,6 @@ import 'package:canokey_console/helper/utils/smartcard.dart'; import 'package:canokey_console/helper/utils/rust_license.dart'; import 'package:canokey_console/helper/utils/screenshot_mode.dart'; import 'package:canokey_console/helper/utils/logging.dart'; -import 'package:canokey_console/helper/utils/fido2_backend.dart'; import 'package:canokey_console/routes.dart'; import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:canokey_console/views/layout/layout.dart'; @@ -45,7 +44,6 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await loadSnapChineseFont(); await RustLib.init(); - await initializeFido2Backend(); await LocalStorage.init(); AppStyle.init(); ThemeCustomizer.instance.currentLanguage = Language.languages[1]; @@ -64,7 +62,6 @@ Future main() async { await loadSnapChineseFont(); await RustLib.init(); - await initializeFido2Backend(); await LocalStorage.init(); AppStyle.init(); Language.init(); diff --git a/lib/models/webauthn.dart b/lib/models/webauthn.dart index 8b1145b..4ff6de2 100644 --- a/lib/models/webauthn.dart +++ b/lib/models/webauthn.dart @@ -1,7 +1,5 @@ import 'dart:typed_data'; -import 'package:fido2/fido2.dart'; - class WebAuthnSm2Config { final bool enabled; final int curveId; @@ -81,7 +79,7 @@ class WebAuthnItem { String userName; String userDisplayName; List userId; - PublicKeyCredentialDescriptor credentialId; + Uint8List credentialId; WebAuthnItem({ required this.rpId, diff --git a/lib/src/rust/api/protocol.dart b/lib/src/rust/api/protocol.dart index fec9cc9..4f64888 100644 --- a/lib/src/rust/api/protocol.dart +++ b/lib/src/rust/api/protocol.dart @@ -6,10 +6,69 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `admin_operation`, `admin_progress`, `admin_result`, `bytes`, `drive`, `drive`, `failure`, `from_operation`, `oath_access`, `openpgp_slot`, `pass_slots_data`, `piv_slot` +// These functions are ignored because they are not marked as `pub`: `admin_operation`, `admin_progress`, `admin_result`, `bytes`, `change_pin_with_iv`, `ctap_credentials_data`, `ctap_info_data`, `ctap_rps_data`, `drive`, `drive`, `failure`, `from_operation`, `new`, `new`, `oath_access`, `openpgp_slot`, `pass_slots_data`, `pin_iv`, `pin_token_with_iv`, `piv_slot`, `set_pin_with_iv`, `token` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Inner` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `from` +// Rust type: RustOpaqueMoi> +abstract class CtapPinSession implements RustOpaqueInterface { + /// Change the PIN (subcommand 0x04); IV handling matches [`Self::set_pin`]. + ProtocolOperation changePin({ + required List oldPin, + required List newPin, + }); + + /// Idempotent local cleanup. Does not alter the card or pending operations. + void close(); + + /// Obtain a pinUvAuthToken with explicit permissions (subcommand 0x09): + /// `permissions` is the raw bitfield (credentialManagement = 0x04) and + /// `rp_id`, when present, binds the token to that relying party. The + /// decrypted token is returned as an opaque handle in the final step. + ProtocolOperation getPinTokenWithPermissions({ + required List pin, + required int permissions, + String? rpId, + }); + + /// The pin/UV auth protocol version (1 or 2) this session speaks. + /// Dart picks it from getInfo's advertised pinUvAuthProtocols. + int protocolVersion(); + + /// Set the initial PIN (subcommand 0x03). V2 IVs are generated here from + /// the CSPRNG; PIN bytes enter upstream zeroizing owners immediately. + ProtocolOperation setPin({required List newPin}); +} + +// Rust type: RustOpaqueMoi> +abstract class CtapPinToken implements RustOpaqueInterface { + /// Idempotent local cleanup. Does not alter the card or pending operations. + void close(); + + /// Permanently delete one resident credential by its raw credential ID + /// (sent as a `public-key` descriptor). Never retried or rolled back; + /// an unknown ID surfaces as NotFound (CTAP 0x2E/0x22). + ProtocolOperation deleteCredential({required List credentialId}); + + /// Enumerate one RP's resident credentials. `metadata_only` enables the + /// CanoKey vendor extension (subCommandParams key 0x80): the raw COSE + /// algorithm replaces the public key in responses. Data: see + /// `ctap_credentials_data`. A Begin 0x2E yields an empty result. + ProtocolOperation enumerateCredentials({ + required List rpIdHash, + required bool metadataOnly, + }); + + /// Enumerate relying parties with resident credentials (Begin + GetNext + /// run inside the one operation). A 0x2E NO_CREDENTIALS status yields an + /// empty result, not an error. Data: see `ctap_rps_data`. + ProtocolOperation enumerateRps(); + + /// The pin/UV auth protocol version (1 or 2) of the session that minted + /// this token; credmgmt operations authenticate with it. + int protocolVersion(); +} + // Rust type: RustOpaqueMoi> abstract class ProtocolOperation implements RustOpaqueInterface { /// Retained upstream progress, available even after protocol failure. @@ -31,6 +90,21 @@ abstract class ProtocolOperation implements RustOpaqueInterface { /// Release locally, including on a transport exception. Never retry/rollback. void close(); + /// Start ClientPIN key agreement (subcommand 0x02) for protocol version + /// 1 or 2, generating the ephemeral P-256 scalar from the CSPRNG. The + /// final step carries an opaque [`CtapPinSession`] handle. + static ProtocolOperation ctapBeginPinSession({required int protocol}) => + RustLib.instance.api.crateApiProtocolProtocolOperationCtapBeginPinSession( + protocol: protocol, + ); + + /// authenticatorGetInfo (0x04), profile-free with its own SELECT. The + /// final `data` carries the parsed fields Dart needs (credMgmt/clientPin + /// tri-states, forcePinChange, minPinLength, advertised pinUvAuthProtocol + /// versions); see `ctap_info_data` for the encoding. + static ProtocolOperation ctapGetInfo() => + RustLib.instance.api.crateApiProtocolProtocolOperationCtapGetInfo(); + /// Select the FIDO2 applet only; the caller owns the selected context. static ProtocolOperation ctapSelectApplication() => RustLib.instance.api .crateApiProtocolProtocolOperationCtapSelectApplication(); @@ -643,6 +717,9 @@ enum PivCredentialOperation { enum PivReadOperation { select, version, algorithmConfiguration, pinStatus } /// Structured, payload-free protocol failure. Transport errors remain in Dart. +/// For CTAP-level failures `status_word` carries the raw CTAP status byte +/// widened to u16 (for example 0x0031 for PIN_INVALID), NOT an ISO 7816 +/// status word; see canokey-ctap's status table. class ProtocolError { final String kind; final String phase; @@ -686,6 +763,8 @@ class ProtocolStep { final ProtocolError? error; final ProtocolProfile? profile; final AdminResult? admin; + final CtapPinSession? pinSession; + final CtapPinToken? pinToken; const ProtocolStep({ this.command, @@ -693,6 +772,8 @@ class ProtocolStep { this.error, this.profile, this.admin, + this.pinSession, + this.pinToken, }); @override @@ -701,7 +782,9 @@ class ProtocolStep { data.hashCode ^ error.hashCode ^ profile.hashCode ^ - admin.hashCode; + admin.hashCode ^ + pinSession.hashCode ^ + pinToken.hashCode; @override bool operator ==(Object other) => @@ -712,5 +795,7 @@ class ProtocolStep { data == other.data && error == other.error && profile == other.profile && - admin == other.admin; + admin == other.admin && + pinSession == other.pinSession && + pinToken == other.pinToken; } diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 232c71e..e057924 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.13.0'; @override - int get rustContentHash => -2116397449; + int get rustContentHash => 1993561274; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -81,6 +81,49 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + ProtocolOperation crateApiProtocolCtapPinSessionChangePin({ + required CtapPinSession that, + required List oldPin, + required List newPin, + }); + + void crateApiProtocolCtapPinSessionClose({required CtapPinSession that}); + + ProtocolOperation crateApiProtocolCtapPinSessionGetPinTokenWithPermissions({ + required CtapPinSession that, + required List pin, + required int permissions, + String? rpId, + }); + + int crateApiProtocolCtapPinSessionProtocolVersion({ + required CtapPinSession that, + }); + + ProtocolOperation crateApiProtocolCtapPinSessionSetPin({ + required CtapPinSession that, + required List newPin, + }); + + void crateApiProtocolCtapPinTokenClose({required CtapPinToken that}); + + ProtocolOperation crateApiProtocolCtapPinTokenDeleteCredential({ + required CtapPinToken that, + required List credentialId, + }); + + ProtocolOperation crateApiProtocolCtapPinTokenEnumerateCredentials({ + required CtapPinToken that, + required List rpIdHash, + required bool metadataOnly, + }); + + ProtocolOperation crateApiProtocolCtapPinTokenEnumerateRps({ + required CtapPinToken that, + }); + + int crateApiProtocolCtapPinTokenProtocolVersion({required CtapPinToken that}); + AdminProgress? crateApiProtocolProtocolOperationAdminProgress({ required ProtocolOperation that, }); @@ -98,6 +141,12 @@ abstract class RustLibApi extends BaseApi { required ProtocolOperation that, }); + ProtocolOperation crateApiProtocolProtocolOperationCtapBeginPinSession({ + required int protocol, + }); + + ProtocolOperation crateApiProtocolProtocolOperationCtapGetInfo(); + ProtocolOperation crateApiProtocolProtocolOperationCtapSelectApplication(); ProtocolOperation crateApiProtocolProtocolOperationCtapTransceive({ @@ -681,6 +730,23 @@ abstract class RustLibApi extends BaseApi { required List signature, }); + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CtapPinSession; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CtapPinSession; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CtapPinSessionPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CtapPinToken; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CtapPinToken; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_CtapPinTokenPtr; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_ProtocolOperation; @@ -708,6 +774,336 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + ProtocolOperation crateApiProtocolCtapPinSessionChangePin({ + required CtapPinSession that, + required List oldPin, + required List newPin, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(oldPin, serializer); + sse_encode_list_prim_u_8_loose(newPin, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinSessionChangePinConstMeta, + argValues: [that, oldPin, newPin], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinSessionChangePinConstMeta => + const TaskConstMeta( + debugName: "CtapPinSession_change_pin", + argNames: ["that", "oldPin", "newPin"], + ); + + @override + void crateApiProtocolCtapPinSessionClose({required CtapPinSession that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinSessionCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinSessionCloseConstMeta => + const TaskConstMeta( + debugName: "CtapPinSession_close", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolCtapPinSessionGetPinTokenWithPermissions({ + required CtapPinSession that, + required List pin, + required int permissions, + String? rpId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(pin, serializer); + sse_encode_u_8(permissions, serializer); + sse_encode_opt_String(rpId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolCtapPinSessionGetPinTokenWithPermissionsConstMeta, + argValues: [that, pin, permissions, rpId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolCtapPinSessionGetPinTokenWithPermissionsConstMeta => + const TaskConstMeta( + debugName: "CtapPinSession_get_pin_token_with_permissions", + argNames: ["that", "pin", "permissions", "rpId"], + ); + + @override + int crateApiProtocolCtapPinSessionProtocolVersion({ + required CtapPinSession that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_u_8, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinSessionProtocolVersionConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinSessionProtocolVersionConstMeta => + const TaskConstMeta( + debugName: "CtapPinSession_protocol_version", + argNames: ["that"], + ); + + @override + ProtocolOperation crateApiProtocolCtapPinSessionSetPin({ + required CtapPinSession that, + required List newPin, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(newPin, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinSessionSetPinConstMeta, + argValues: [that, newPin], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinSessionSetPinConstMeta => + const TaskConstMeta( + debugName: "CtapPinSession_set_pin", + argNames: ["that", "newPin"], + ); + + @override + void crateApiProtocolCtapPinTokenClose({required CtapPinToken that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinTokenCloseConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinTokenCloseConstMeta => + const TaskConstMeta(debugName: "CtapPinToken_close", argNames: ["that"]); + + @override + ProtocolOperation crateApiProtocolCtapPinTokenDeleteCredential({ + required CtapPinToken that, + required List credentialId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(credentialId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinTokenDeleteCredentialConstMeta, + argValues: [that, credentialId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinTokenDeleteCredentialConstMeta => + const TaskConstMeta( + debugName: "CtapPinToken_delete_credential", + argNames: ["that", "credentialId"], + ); + + @override + ProtocolOperation crateApiProtocolCtapPinTokenEnumerateCredentials({ + required CtapPinToken that, + required List rpIdHash, + required bool metadataOnly, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(rpIdHash, serializer); + sse_encode_bool(metadataOnly, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinTokenEnumerateCredentialsConstMeta, + argValues: [that, rpIdHash, metadataOnly], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolCtapPinTokenEnumerateCredentialsConstMeta => + const TaskConstMeta( + debugName: "CtapPinToken_enumerate_credentials", + argNames: ["that", "rpIdHash", "metadataOnly"], + ); + + @override + ProtocolOperation crateApiProtocolCtapPinTokenEnumerateRps({ + required CtapPinToken that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinTokenEnumerateRpsConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinTokenEnumerateRpsConstMeta => + const TaskConstMeta( + debugName: "CtapPinToken_enumerate_rps", + argNames: ["that"], + ); + + @override + int crateApiProtocolCtapPinTokenProtocolVersion({ + required CtapPinToken that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_u_8, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolCtapPinTokenProtocolVersionConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolCtapPinTokenProtocolVersionConstMeta => + const TaskConstMeta( + debugName: "CtapPinToken_protocol_version", + argNames: ["that"], + ); + @override AdminProgress? crateApiProtocolProtocolOperationAdminProgress({ required ProtocolOperation that, @@ -720,7 +1116,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_admin_progress, @@ -753,7 +1149,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(response, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; }, codec: SseCodec( decodeSuccessData: sse_decode_protocol_step, @@ -781,7 +1177,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_bootstrap_identity_step(step, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 3)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; }, codec: SseCodec( decodeSuccessData: @@ -814,7 +1210,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 4)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -833,13 +1229,70 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that"], ); + @override + ProtocolOperation crateApiProtocolProtocolOperationCtapBeginPinSession({ + required int protocol, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_u_8(protocol, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolOperationCtapBeginPinSessionConstMeta, + argValues: [protocol], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolOperationCtapBeginPinSessionConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ctap_begin_pin_session", + argNames: ["protocol"], + ); + + @override + ProtocolOperation crateApiProtocolProtocolOperationCtapGetInfo() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: kCrateApiProtocolProtocolOperationCtapGetInfoConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiProtocolProtocolOperationCtapGetInfoConstMeta => + const TaskConstMeta( + debugName: "ProtocolOperation_ctap_get_info", + argNames: [], + ); + @override ProtocolOperation crateApiProtocolProtocolOperationCtapSelectApplication() { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 5)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; }, codec: SseCodec( decodeSuccessData: @@ -870,7 +1323,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(message, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 6)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: @@ -899,7 +1352,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(message, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; }, codec: SseCodec( decodeSuccessData: @@ -927,7 +1380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 8)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; }, codec: SseCodec( decodeSuccessData: @@ -955,7 +1408,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; }, codec: SseCodec( decodeSuccessData: @@ -985,7 +1438,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(message, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; }, codec: SseCodec( decodeSuccessData: @@ -1015,7 +1468,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_piv_read_operation(kind, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; }, codec: SseCodec( decodeSuccessData: @@ -1041,7 +1494,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; }, codec: SseCodec( decodeSuccessData: @@ -1067,7 +1520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; }, codec: SseCodec( decodeSuccessData: @@ -1099,7 +1552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; }, codec: SseCodec( decodeSuccessData: sse_decode_protocol_step, @@ -1142,7 +1595,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(index, serializer); sse_encode_u_8(value, serializer); sse_encode_bool(existing, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!; }, codec: SseCodec( decodeSuccessData: @@ -1180,7 +1633,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_box_autoadd_admin_configuration_patch(patch, serializer); sse_encode_opt_list_prim_u_8_strict(pin, serializer); sse_encode_bool(existing, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; }, codec: SseCodec( decodeSuccessData: @@ -1216,7 +1669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_opt_list_prim_u_8_strict(pin, serializer); sse_encode_bool(existing, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; }, codec: SseCodec( decodeSuccessData: @@ -1254,7 +1707,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_admin_read_operation(kind, serializer); sse_encode_opt_list_prim_u_8_strict(pin, serializer); sse_encode_bool(existing, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; }, codec: SseCodec( decodeSuccessData: @@ -1298,7 +1751,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(appendEnter, serializer); sse_encode_opt_list_prim_u_8_strict(pin, serializer); sse_encode_bool(existing, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!; }, codec: SseCodec( decodeSuccessData: @@ -1336,7 +1789,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -1367,7 +1820,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, @@ -1398,7 +1851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -1443,7 +1896,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(format, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; }, codec: SseCodec( decodeSuccessData: @@ -1501,7 +1954,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(format, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; }, codec: SseCodec( decodeSuccessData: @@ -1543,7 +1996,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_opt_list_prim_u_8_strict(key, serializer); sse_encode_opt_list_prim_u_8_strict(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; }, codec: SseCodec( decodeSuccessData: @@ -1581,7 +2034,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(name, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!; }, codec: SseCodec( decodeSuccessData: @@ -1617,7 +2070,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; }, codec: SseCodec( decodeSuccessData: @@ -1669,7 +2122,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_32(initialCounter, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!; }, codec: SseCodec( decodeSuccessData: @@ -1733,7 +2186,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(new_, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 41)!; }, codec: SseCodec( decodeSuccessData: @@ -1765,7 +2218,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42)!; }, codec: SseCodec( decodeSuccessData: @@ -1803,7 +2256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(oldKey, serializer); sse_encode_list_prim_u_8_loose(newKey, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; }, codec: SseCodec( decodeSuccessData: @@ -1845,7 +2298,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(name, serializer); sse_encode_opt_list_prim_u_8_strict(accessKey, serializer); sse_encode_opt_list_prim_u_8_strict(accessChallenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; }, codec: SseCodec( decodeSuccessData: @@ -1888,7 +2341,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; }, codec: SseCodec( decodeSuccessData: @@ -1922,7 +2375,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46)!; }, codec: SseCodec( decodeSuccessData: @@ -1960,7 +2413,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(reference, serializer); sse_encode_list_prim_u_8_loose(old, serializer); sse_encode_list_prim_u_8_loose(new_, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; }, codec: SseCodec( decodeSuccessData: @@ -1998,7 +2451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(slot, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48)!; }, codec: SseCodec( decodeSuccessData: @@ -2033,7 +2486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(reference, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 49)!; }, codec: SseCodec( decodeSuccessData: @@ -2067,7 +2520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(reference, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 50)!; }, codec: SseCodec( decodeSuccessData: @@ -2101,7 +2554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(slot, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; }, codec: SseCodec( decodeSuccessData: @@ -2137,7 +2590,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_16(tag, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 40)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 52)!; }, codec: SseCodec( decodeSuccessData: @@ -2173,7 +2626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(retries, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 41)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!; }, codec: SseCodec( decodeSuccessData: @@ -2208,7 +2661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 42)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54)!; }, codec: SseCodec( decodeSuccessData: @@ -2244,7 +2697,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(newPin, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; }, codec: SseCodec( decodeSuccessData: @@ -2282,7 +2735,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(code, serializer); sse_encode_list_prim_u_8_loose(newPin, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 56)!; }, codec: SseCodec( decodeSuccessData: @@ -2320,7 +2773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(reference, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 57)!; }, codec: SseCodec( decodeSuccessData: @@ -2358,7 +2811,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(slot, serializer); sse_encode_list_prim_u_8_loose(certificate, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 46)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 58)!; }, codec: SseCodec( decodeSuccessData: @@ -2396,7 +2849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(language, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; }, codec: SseCodec( decodeSuccessData: @@ -2434,7 +2887,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(login, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 48)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 60)!; }, codec: SseCodec( decodeSuccessData: @@ -2471,7 +2924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(name, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 49)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 61)!; }, codec: SseCodec( decodeSuccessData: @@ -2507,7 +2960,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_opt_list_prim_u_8_strict(resetCode, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 50)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 62)!; }, codec: SseCodec( decodeSuccessData: @@ -2545,7 +2998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(sex, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!; }, codec: SseCodec( decodeSuccessData: @@ -2582,7 +3035,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_bool(reuse, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 52)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!; }, codec: SseCodec( decodeSuccessData: @@ -2620,7 +3073,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(seconds, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 53)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!; }, codec: SseCodec( decodeSuccessData: @@ -2660,7 +3113,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(slot, serializer); sse_encode_u_8(policy, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; }, codec: SseCodec( decodeSuccessData: @@ -2698,7 +3151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(url, serializer); sse_encode_list_prim_u_8_loose(password, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!; }, codec: SseCodec( decodeSuccessData: @@ -2732,7 +3185,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(wireId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 56)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 68)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_box_autoadd_u_8, @@ -2767,7 +3220,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(slot, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 57)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!; }, codec: SseCodec( decodeSuccessData: @@ -2803,7 +3256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(key, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 58)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!; }, codec: SseCodec( decodeSuccessData: @@ -2839,7 +3292,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(objectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 59)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 71)!; }, codec: SseCodec( decodeSuccessData: @@ -2877,7 +3330,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_piv_credential_operation(kind, serializer); sse_encode_list_prim_u_8_loose(current, serializer); sse_encode_list_prim_u_8_loose(replacement, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 60)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 72)!; }, codec: SseCodec( decodeSuccessData: @@ -2913,7 +3366,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(slot, serializer); sse_encode_list_prim_u_8_loose(ciphertext, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 61)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 73)!; }, codec: SseCodec( decodeSuccessData: @@ -2951,7 +3404,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(slot, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(ciphertext, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 62)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 74)!; }, codec: SseCodec( decodeSuccessData: @@ -2985,7 +3438,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(objectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 75)!; }, codec: SseCodec( decodeSuccessData: @@ -3021,7 +3474,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(slot, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 76)!; }, codec: SseCodec( decodeSuccessData: @@ -3059,7 +3512,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(slot, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(peer, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; }, codec: SseCodec( decodeSuccessData: @@ -3099,7 +3552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_u_8(pinPolicy, serializer); sse_encode_u_8(touchPolicy, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; }, codec: SseCodec( decodeSuccessData: @@ -3141,7 +3594,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(scalar, serializer); sse_encode_u_8(pinPolicy, serializer); sse_encode_u_8(touchPolicy, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 79)!; }, codec: SseCodec( decodeSuccessData: @@ -3188,7 +3641,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(seed, serializer); sse_encode_u_8(pinPolicy, serializer); sse_encode_u_8(touchPolicy, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 68)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 80)!; }, codec: SseCodec( decodeSuccessData: @@ -3231,7 +3684,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(seed, serializer); sse_encode_u_8(pinPolicy, serializer); sse_encode_u_8(touchPolicy, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 81)!; }, codec: SseCodec( decodeSuccessData: @@ -3281,7 +3734,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(qinv, serializer); sse_encode_u_8(pinPolicy, serializer); sse_encode_u_8(touchPolicy, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 82)!; }, codec: SseCodec( decodeSuccessData: @@ -3337,7 +3790,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_u_8(reference, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 71)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; }, codec: SseCodec( decodeSuccessData: @@ -3369,7 +3822,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 72)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 84)!; }, codec: SseCodec( decodeSuccessData: @@ -3407,7 +3860,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(source, serializer); sse_encode_u_8(target, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 73)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 85)!; }, codec: SseCodec( decodeSuccessData: @@ -3441,7 +3894,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(objectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 74)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; }, codec: SseCodec( decodeSuccessData: @@ -3477,7 +3930,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(pinRetries, serializer); sse_encode_u_8(pukRetries, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 75)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; }, codec: SseCodec( decodeSuccessData: @@ -3513,7 +3966,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(raw, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 76)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; }, codec: SseCodec( decodeSuccessData: @@ -3551,7 +4004,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(slot, serializer); sse_encode_String(name, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; }, codec: SseCodec( decodeSuccessData: @@ -3592,7 +4045,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_u_8(touch, serializer); sse_encode_bool(updateProtected, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 90)!; }, codec: SseCodec( decodeSuccessData: @@ -3633,7 +4086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(input, serializer); sse_encode_u_8(inputKind, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 79)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; }, codec: SseCodec( decodeSuccessData: @@ -3673,7 +4126,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(mode, serializer); sse_encode_list_prim_u_8_loose(message, serializer); sse_encode_opt_list_prim_u_8_strict(userId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 80)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 92)!; }, codec: SseCodec( decodeSuccessData: @@ -3719,7 +4172,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(userId, serializer); sse_encode_opt_list_prim_u_8_strict(peerId, serializer); sse_encode_u_16(keyLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 81)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; }, codec: SseCodec( decodeSuccessData: @@ -3773,7 +4226,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(objectId, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 82)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; }, codec: SseCodec( decodeSuccessData: @@ -3805,7 +4258,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, @@ -3837,7 +4290,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(cardData, serializer); sse_encode_bool(generatedResponse, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 84)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -3863,7 +4316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(pngFile, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 85)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -3895,7 +4348,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -3927,7 +4380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(certificationRequestInfo, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 99)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -3959,7 +4412,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(tbsCertificate, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 100, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -3989,7 +4446,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 101, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4011,7 +4472,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 90)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 102, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -4036,7 +4501,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(bytes, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 103, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_import_file_data, @@ -4066,7 +4535,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 92)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 104, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -4092,7 +4565,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(der, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 105, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -4118,7 +4595,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(pem, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 106, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_x_509_cert_data, @@ -4152,7 +4633,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(salt, serializer); sse_encode_u_32(iterations, serializer); sse_encode_u_32(keyLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 107, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4186,7 +4671,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(expectedPublicKey, serializer); sse_encode_u_8(slot, serializer); sse_encode_i_64(nowUnix, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 108, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -4224,7 +4713,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(country, serializer); sse_encode_list_prim_u_8_loose(subjectPublicKeyInfo, serializer); sse_encode_list_String(subjectAlternativeNames, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 109, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4270,7 +4763,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(data, serializer); sse_encode_opt_list_prim_u_8_strict(publicKey, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 110, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4301,7 +4798,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { params, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 99)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 111, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4330,7 +4831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 112, )!; }, codec: SseCodec( @@ -4357,7 +4858,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 113, )!; }, codec: SseCodec( @@ -4384,7 +4885,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 114, )!; }, codec: SseCodec( @@ -4415,7 +4916,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 115, )!; }, codec: SseCodec( @@ -4449,7 +4950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 116, )!; }, codec: SseCodec( @@ -4486,7 +4987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 117, )!; }, codec: SseCodec( @@ -4506,6 +5007,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["algorithm", "publicKey", "data", "signature"], ); + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CtapPinSession => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CtapPinSession => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CtapPinToken => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CtapPinToken => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_ProtocolOperation => wire .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; @@ -4522,6 +5039,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { get rust_arc_decrement_strong_count_ProtocolProfile => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile; + @protected + CtapPinSession + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CtapPinToken + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalDcoDecode(raw as List); + } + @protected ProtocolOperation dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -4540,6 +5075,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); } + @protected + CtapPinSession + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CtapPinToken + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalDcoDecode(raw as List); + } + @protected ProtocolOperation dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -4558,6 +5111,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); } + @protected + CtapPinSession + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CtapPinToken + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalDcoDecode(raw as List); + } + @protected ProtocolProfile dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -4567,6 +5138,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ProtocolProfileImpl.frbInternalDcoDecode(raw as List); } + @protected + CtapPinSession + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CtapPinToken + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalDcoDecode(raw as List); + } + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -4662,6 +5251,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BootstrapIdentityStep.values[raw as int]; } + @protected + CtapPinSession + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + raw, + ); + } + + @protected + CtapPinToken + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + raw, + ); + } + @protected ProtocolProfile dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -4772,6 +5383,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_String(raw); } + @protected + CtapPinSession? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + raw, + ); + } + + @protected + CtapPinToken? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + raw, + ); + } + @protected ProtocolProfile? dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -4911,8 +5548,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ProtocolStep dco_decode_protocol_step(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 5) - throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + if (arr.length != 7) + throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return ProtocolStep( command: dco_decode_opt_list_prim_u_8_strict(arr[0]), data: dco_decode_opt_list_prim_u_8_strict(arr[1]), @@ -4922,6 +5559,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { arr[3], ), admin: dco_decode_opt_box_autoadd_admin_result(arr[4]), + pinSession: + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + arr[5], + ), + pinToken: + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + arr[6], + ), ); } @@ -5002,9 +5647,81 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + CtapPinSession + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CtapPinToken + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolOperation + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolOperationImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + ProtocolProfile + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return ProtocolProfileImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CtapPinSession + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CtapPinToken + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected ProtocolOperation - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5016,7 +5733,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @protected ProtocolProfile - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5027,24 +5744,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - ProtocolOperation - sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( + CtapPinSession + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - return ProtocolOperationImpl.frbInternalSseDecode( + return CtapPinSessionImpl.frbInternalSseDecode( sse_decode_usize(deserializer), sse_decode_i_32(deserializer), ); } @protected - ProtocolProfile - sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + CtapPinToken + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( SseDeserializer deserializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - return ProtocolProfileImpl.frbInternalSseDecode( + return CtapPinTokenImpl.frbInternalSseDecode( sse_decode_usize(deserializer), sse_decode_i_32(deserializer), ); @@ -5062,6 +5779,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + CtapPinSession + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinSessionImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CtapPinToken + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CtapPinTokenImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected ProtocolOperation sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -5172,6 +5913,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BootstrapIdentityStep.values[inner]; } + @protected + CtapPinSession + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + deserializer, + )); + } + + @protected + CtapPinToken + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + deserializer, + )); + } + @protected ProtocolProfile sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -5307,6 +6070,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + CtapPinSession? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + deserializer, + )); + } else { + return null; + } + } + + @protected + CtapPinToken? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + deserializer, + )); + } else { + return null; + } + } + @protected ProtocolProfile? sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -5525,12 +6320,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { deserializer, ); var var_admin = sse_decode_opt_box_autoadd_admin_result(deserializer); + var var_pinSession = + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + deserializer, + ); + var var_pinToken = + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + deserializer, + ); return ProtocolStep( command: var_command, data: var_data, error: var_error, profile: var_profile, admin: var_admin, + pinSession: var_pinSession, + pinToken: var_pinToken, ); } @@ -5633,6 +6438,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinSessionImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinTokenImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -5659,6 +6490,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinSessionImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinTokenImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + @protected void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -5685,6 +6542,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinSessionImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinTokenImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -5698,6 +6581,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinSessionImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CtapPinTokenImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -5798,6 +6707,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_32(self.index, serializer); } + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + self, + serializer, + ); + } + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + self, + serializer, + ); + } + @protected void sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -5945,6 +6880,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + self, + serializer, + ); + } + } + + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + self, + serializer, + ); + } + } + @protected void sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -6143,6 +7112,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_opt_box_autoadd_admin_result(self.admin, serializer); + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + self.pinSession, + serializer, + ); + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + self.pinToken, + serializer, + ); } @protected @@ -6213,6 +7190,123 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } +@sealed +class CtapPinSessionImpl extends RustOpaque implements CtapPinSession { + // Not to be used by end users + CtapPinSessionImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + CtapPinSessionImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_CtapPinSession, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_CtapPinSession, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_CtapPinSessionPtr, + ); + + /// Change the PIN (subcommand 0x04); IV handling matches [`Self::set_pin`]. + ProtocolOperation changePin({ + required List oldPin, + required List newPin, + }) => RustLib.instance.api.crateApiProtocolCtapPinSessionChangePin( + that: this, + oldPin: oldPin, + newPin: newPin, + ); + + /// Idempotent local cleanup. Does not alter the card or pending operations. + void close() => + RustLib.instance.api.crateApiProtocolCtapPinSessionClose(that: this); + + /// Obtain a pinUvAuthToken with explicit permissions (subcommand 0x09): + /// `permissions` is the raw bitfield (credentialManagement = 0x04) and + /// `rp_id`, when present, binds the token to that relying party. The + /// decrypted token is returned as an opaque handle in the final step. + ProtocolOperation getPinTokenWithPermissions({ + required List pin, + required int permissions, + String? rpId, + }) => RustLib.instance.api + .crateApiProtocolCtapPinSessionGetPinTokenWithPermissions( + that: this, + pin: pin, + permissions: permissions, + rpId: rpId, + ); + + /// The pin/UV auth protocol version (1 or 2) this session speaks. + /// Dart picks it from getInfo's advertised pinUvAuthProtocols. + int protocolVersion() => RustLib.instance.api + .crateApiProtocolCtapPinSessionProtocolVersion(that: this); + + /// Set the initial PIN (subcommand 0x03). V2 IVs are generated here from + /// the CSPRNG; PIN bytes enter upstream zeroizing owners immediately. + ProtocolOperation setPin({required List newPin}) => RustLib.instance.api + .crateApiProtocolCtapPinSessionSetPin(that: this, newPin: newPin); +} + +@sealed +class CtapPinTokenImpl extends RustOpaque implements CtapPinToken { + // Not to be used by end users + CtapPinTokenImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + CtapPinTokenImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_CtapPinToken, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_CtapPinToken, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_CtapPinTokenPtr, + ); + + /// Idempotent local cleanup. Does not alter the card or pending operations. + void close() => + RustLib.instance.api.crateApiProtocolCtapPinTokenClose(that: this); + + /// Permanently delete one resident credential by its raw credential ID + /// (sent as a `public-key` descriptor). Never retried or rolled back; + /// an unknown ID surfaces as NotFound (CTAP 0x2E/0x22). + ProtocolOperation deleteCredential({required List credentialId}) => + RustLib.instance.api.crateApiProtocolCtapPinTokenDeleteCredential( + that: this, + credentialId: credentialId, + ); + + /// Enumerate one RP's resident credentials. `metadata_only` enables the + /// CanoKey vendor extension (subCommandParams key 0x80): the raw COSE + /// algorithm replaces the public key in responses. Data: see + /// `ctap_credentials_data`. A Begin 0x2E yields an empty result. + ProtocolOperation enumerateCredentials({ + required List rpIdHash, + required bool metadataOnly, + }) => RustLib.instance.api.crateApiProtocolCtapPinTokenEnumerateCredentials( + that: this, + rpIdHash: rpIdHash, + metadataOnly: metadataOnly, + ); + + /// Enumerate relying parties with resident credentials (Begin + GetNext + /// run inside the one operation). A 0x2E NO_CREDENTIALS status yields an + /// empty result, not an error. Data: see `ctap_rps_data`. + ProtocolOperation enumerateRps() => + RustLib.instance.api.crateApiProtocolCtapPinTokenEnumerateRps(that: this); + + /// The pin/UV auth protocol version (1 or 2) of the session that minted + /// this token; credmgmt operations authenticate with it. + int protocolVersion() => RustLib.instance.api + .crateApiProtocolCtapPinTokenProtocolVersion(that: this); +} + @sealed class ProtocolOperationImpl extends RustOpaque implements ProtocolOperation { // Not to be used by end users diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index 022a3a4..582ef47 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -21,6 +21,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CtapPinSessionPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSessionPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CtapPinTokenPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinTokenPtr; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperationPtr; @@ -29,6 +37,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { get rust_arc_decrement_strong_count_ProtocolProfilePtr => wire ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfilePtr; + @protected + CtapPinSession + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -41,6 +61,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + CtapPinSession + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -53,12 +85,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + CtapPinSession + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( dynamic raw, ); + @protected + CtapPinSession + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -98,6 +154,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BootstrapIdentityStep dco_decode_bootstrap_identity_step(dynamic raw); + @protected + CtapPinSession + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -155,6 +223,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + CtapPinSession? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile? dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -234,6 +314,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData dco_decode_x_509_cert_data(dynamic raw); + @protected + CtapPinSession + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -246,6 +338,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -258,12 +362,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -309,6 +437,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -376,6 +516,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + CtapPinSession? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile? sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -469,6 +621,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData sse_decode_x_509_cert_data(SseDeserializer deserializer); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -483,6 +649,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -497,6 +677,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -504,6 +698,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -557,6 +765,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -636,6 +858,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession? self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -763,6 +999,74 @@ class RustLibWire implements BaseWire { RustLibWire(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSessionPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSessionPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSessionPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSessionPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinTokenPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinTokenPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinTokenPtr = + _lookup)>>( + 'frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinTokenPtr + .asFunction)>(); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( ffi.Pointer ptr, diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index bee6c93..060de29 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -23,6 +23,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CtapPinSessionPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CtapPinTokenPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_ProtocolOperationPtr => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation; @@ -31,6 +39,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { get rust_arc_decrement_strong_count_ProtocolProfilePtr => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile; + @protected + CtapPinSession + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -43,6 +63,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + CtapPinSession + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -55,12 +87,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + CtapPinSession + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( dynamic raw, ); + @protected + CtapPinSession + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolOperation dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -100,6 +156,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BootstrapIdentityStep dco_decode_bootstrap_identity_step(dynamic raw); + @protected + CtapPinSession + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken + dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile dco_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -157,6 +225,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + CtapPinSession? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + dynamic raw, + ); + + @protected + CtapPinToken? + dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + dynamic raw, + ); + @protected ProtocolProfile? dco_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -236,6 +316,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData dco_decode_x_509_cert_data(dynamic raw); + @protected + CtapPinSession + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -248,6 +340,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -260,12 +364,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolOperation sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -311,6 +439,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + CtapPinSession + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken + sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile sse_decode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -378,6 +518,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + CtapPinSession? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + SseDeserializer deserializer, + ); + + @protected + CtapPinToken? + sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + SseDeserializer deserializer, + ); + @protected ProtocolProfile? sse_decode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -471,6 +623,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected X509CertData sse_decode_x_509_cert_data(SseDeserializer deserializer); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -485,6 +651,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -499,6 +679,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -506,6 +700,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( @@ -559,6 +767,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -638,6 +860,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + CtapPinSession? self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + CtapPinToken? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( @@ -756,6 +992,38 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { class RustLibWire implements BaseWire { RustLibWire.fromExternalLibrary(ExternalLibrary lib); + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr, + ); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( int ptr, @@ -795,6 +1063,26 @@ external RustLibWasmModule get wasmModule; @JS() @anonymous extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + int ptr, + ); + external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( int ptr, diff --git a/pubspec.lock b/pubspec.lock index 0c69937..133455b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,14 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - cbor: - dependency: "direct dev" - description: - name: cbor - sha256: "2c5c37650f0a2d25149f03e748ab7b2857787bde338f95fe947738b80d713da2" - url: "https://pub.dev" - source: hosted - version: "6.5.1" ccid: dependency: "direct main" description: @@ -233,14 +225,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2" - fido2: - dependency: "direct main" - description: - name: fido2 - sha256: "7d9c76ddccf9347998f8fd6b9df2b72624bc097526a76fb68202bf49efb67a61" - url: "https://pub.dev" - source: hosted - version: "2.0.0" file: dependency: transitive description: @@ -389,14 +373,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.1" - hex: - dependency: transitive - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" hooks: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8dbd8b8..f1abab2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,6 @@ dependencies: mobile_scanner: ^7.1.3 ndef: ^0.4.0 flutter_webrtc: ^1.6.1 - fido2: 2.0.0 ccid: ^0.1.9 device_info_plus: ^13.1.0 file_saver: ^0.4.0 @@ -46,7 +45,6 @@ dependencies: share_plus: ^13.3.0 dev_dependencies: - cbor: ^6.5.1 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index dd325dd..1008f66 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,7 +355,7 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-admin", "canokey-compat", @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-compat", "canokey-protocol", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-protocol", ] @@ -387,15 +387,22 @@ dependencies = [ [[package]] name = "canokey-ctap" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ + "aes 0.8.4", "canokey-protocol", + "cbc", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "sha2 0.10.9", + "zeroize", ] [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-compat", "canokey-protocol", @@ -407,7 +414,7 @@ dependencies = [ [[package]] name = "canokey-ndef" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-protocol", ] @@ -415,7 +422,7 @@ dependencies = [ [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-compat", "canokey-protocol", @@ -428,7 +435,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "canokey-compat", "canokey-key", @@ -438,7 +445,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -459,7 +466,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=95e1930ea86eb248d0295bce9880cee5c65fd85e#95e1930ea86eb248d0295bce9880cee5c65fd85e" +source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" dependencies = [ "thiserror 2.0.20", "zeroize", @@ -750,22 +757,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto 0.2.9", - "rustc_version", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "5.0.0" @@ -776,7 +767,7 @@ dependencies = [ "cpufeatures 0.3.1", "curve25519-dalek-derive", "digest 0.11.3", - "fiat-crypto 0.3.0", + "fiat-crypto", "rustc_version", "subtle", "zeroize", @@ -961,9 +952,7 @@ dependencies = [ "der 0.7.10", "digest 0.10.7", "elliptic-curve 0.13.8", - "rfc6979 0.4.0", "signature 2.2.0", - "spki 0.7.3", ] [[package]] @@ -981,16 +970,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - [[package]] name = "ed25519" version = "3.0.0" @@ -1001,28 +980,14 @@ dependencies = [ "signature 3.0.0", ] -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "ed25519-dalek" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ - "curve25519-dalek 5.0.0", - "ed25519 3.0.0", + "curve25519-dalek", + "ed25519", "serde", "sha2 0.11.0", "signature 3.0.0", @@ -1186,42 +1151,12 @@ dependencies = [ "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "fiat-crypto" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" -[[package]] -name = "fido2_crypto" -version = "0.1.0" -source = "git+https://github.com/nfcim/fido2?rev=623bf7de456bb4475b67c14a90bcc46b81d1095d#4912d3ede19a4b8159dfc00938de5dbea682e079" -dependencies = [ - "aes 0.8.4", - "cbc", - "der 0.7.10", - "ed25519-dalek 2.2.0", - "getrandom 0.2.17", - "hkdf", - "hmac 0.12.1", - "ml-dsa", - "p256 0.13.2", - "serde", - "serde_json", - "sha2 0.10.9", - "sm2", - "sm3 0.4.2", - "subtle", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "find-msvc-tools" version = "0.1.12" @@ -1537,7 +1472,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ - "ctutils", "subtle", "typenum", "zeroize", @@ -1724,16 +1658,6 @@ dependencies = [ "wnaf", ] -[[package]] -name = "keccak" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1857,34 +1781,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "ml-dsa" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" -dependencies = [ - "crypto-common 0.2.2", - "ctutils", - "hybrid-array", - "module-lattice", - "pkcs8 0.11.0", - "shake", - "signature 3.0.0", - "zeroize", -] - -[[package]] -name = "module-lattice" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" -dependencies = [ - "ctutils", - "hybrid-array", - "num-traits", - "zeroize", -] - [[package]] name = "moxcms" version = "0.7.11" @@ -2145,7 +2041,7 @@ checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ "ecdsa 0.17.0", "elliptic-curve 0.14.1", - "fiat-crypto 0.3.0", + "fiat-crypto", "primefield", "primeorder 0.14.0", "sha2 0.11.0", @@ -2694,8 +2590,7 @@ dependencies = [ "const-oid 0.10.2", "der 0.8.1", "des 0.9.0", - "ed25519-dalek 3.0.0", - "fido2_crypto", + "ed25519-dalek", "flutter_rust_bridge", "getrandom 0.4.3", "hex-literal", @@ -2837,7 +2732,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", - "serde_derive", ] [[package]] @@ -2860,19 +2754,6 @@ dependencies = [ "syn 3.0.5", ] -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -2936,17 +2817,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "shake" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" -dependencies = [ - "digest 0.11.3", - "keccak", - "sponge-cursor", -] - [[package]] name = "shlex" version = "2.0.1" @@ -3076,12 +2946,6 @@ dependencies = [ "der 0.8.1", ] -[[package]] -name = "sponge-cursor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" - [[package]] name = "subtle" version = "2.6.1" @@ -3618,12 +3482,6 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - [[package]] name = "zune-core" version = "0.4.12" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9f097d4..ce044c7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "95e1930ea86eb248d0295bce9880cee5c65fd85e" } -canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "95e1930ea86eb248d0295bce9880cee5c65fd85e" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "f043d5390ee2f6810fa543f8e1e9f9acac591e7d" } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "f043d5390ee2f6810fa543f8e1e9f9acac591e7d" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } @@ -24,6 +24,7 @@ p256 = { version = "^0.14.0", default-features = false, features = ["ecdsa", "pe p384 = { version = "^0.14.0", default-features = false, features = ["ecdsa", "pem", "pkcs8", "std"] } p521 = { version = "^0.14.0", default-features = false, features = ["ecdsa", "pem", "pkcs8", "std"] } pbkdf2 = { version = "^0.13.0", features = ["hmac"] } +rand = "^0.10.0" rsa = { version = "^0.9.10", default-features = false, features = ["pem", "sha2", "std", "u64_digit"] } rxing = { version = "^0.9.2", features = ["client_support", "decoders", "encoding_rs", "image", "qrcode", "wasm_support"], default-features = false } sha1 = "^0.11.0" @@ -36,17 +37,13 @@ x509-parser = "^0.18.1" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "^0.4.3", features = ["wasm_js"] } -# Keep the wasm-bindgen family compatible with fido2 2.0.0's exact pin. +# Keep the wasm-bindgen family pinned for reproducible FRB web builds. wasm-bindgen = "=0.2.100" js-sys = "=0.3.77" web-sys = "=0.3.77" wasm-bindgen-futures = "=0.4.50" -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -fido2_crypto = { git = "https://github.com/nfcim/fido2", rev = "623bf7de456bb4475b67c14a90bcc46b81d1095d" } - [dev-dependencies] -rand = "^0.10.0" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/rust/THIRD_PARTY_LICENSES.json b/rust/THIRD_PARTY_LICENSES.json index 51ba6ce..1b8fd9b 100644 --- a/rust/THIRD_PARTY_LICENSES.json +++ b/rust/THIRD_PARTY_LICENSES.json @@ -251,12 +251,24 @@ "repository": "", "license": "Apache-2.0" }, + { + "package_name": "canokey-ctap", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, { "package_name": "canokey-key", "package_version": "0.1.0", "repository": "", "license": "Apache-2.0" }, + { + "package_name": "canokey-ndef", + "package_version": "0.1.0", + "repository": "", + "license": "Apache-2.0" + }, { "package_name": "canokey-oath", "package_version": "0.1.0", @@ -299,6 +311,12 @@ "repository": "https://github.com/rust-lang/cfg-if", "license": "MIT OR Apache-2.0" }, + { + "package_name": "chacha20", + "package_version": "0.10.2", + "repository": "https://github.com/RustCrypto/stream-ciphers", + "license": "MIT OR Apache-2.0" + }, { "package_name": "chrono", "package_version": "0.4.45", @@ -443,12 +461,6 @@ "repository": "https://github.com/RustCrypto/utils", "license": "Apache-2.0 OR MIT" }, - { - "package_name": "curve25519-dalek", - "package_version": "4.1.3", - "repository": "https://github.com/dalek-cryptography/curve25519-dalek/tree/main/curve25519-dalek", - "license": "BSD-3-Clause" - }, { "package_name": "curve25519-dalek", "package_version": "5.0.0", @@ -563,24 +575,12 @@ "repository": "https://github.com/RustCrypto/signatures", "license": "Apache-2.0 OR MIT" }, - { - "package_name": "ed25519", - "package_version": "2.2.3", - "repository": "https://github.com/RustCrypto/signatures/tree/master/ed25519", - "license": "Apache-2.0 OR MIT" - }, { "package_name": "ed25519", "package_version": "3.0.0", "repository": "https://github.com/RustCrypto/signatures", "license": "Apache-2.0 OR MIT" }, - { - "package_name": "ed25519-dalek", - "package_version": "2.2.0", - "repository": "https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek", - "license": "BSD-3-Clause" - }, { "package_name": "ed25519-dalek", "package_version": "3.0.0", @@ -671,24 +671,12 @@ "repository": "https://github.com/zkcrypto/ff", "license": "MIT/Apache-2.0" }, - { - "package_name": "fiat-crypto", - "package_version": "0.2.9", - "repository": "https://github.com/mit-plv/fiat-crypto", - "license": "MIT OR Apache-2.0 OR BSD-1-Clause" - }, { "package_name": "fiat-crypto", "package_version": "0.3.0", "repository": "https://github.com/mit-plv/fiat-crypto", "license": "MIT OR Apache-2.0 OR BSD-1-Clause" }, - { - "package_name": "fido2_crypto", - "package_version": "0.1.0", - "repository": "https://github.com/nfcim/fido2", - "license": "MIT" - }, { "package_name": "flagset", "package_version": "0.4.7", @@ -947,12 +935,6 @@ "repository": "https://github.com/rust-itertools/itertools", "license": "MIT OR Apache-2.0" }, - { - "package_name": "itoa", - "package_version": "1.0.18", - "repository": "https://github.com/dtolnay/itoa", - "license": "MIT OR Apache-2.0" - }, { "package_name": "js-sys", "package_version": "0.3.77", @@ -971,12 +953,6 @@ "repository": "https://github.com/RustCrypto/elliptic-curves", "license": "Apache-2.0 OR MIT" }, - { - "package_name": "keccak", - "package_version": "0.2.2", - "repository": "https://github.com/RustCrypto/sponges", - "license": "Apache-2.0 OR MIT" - }, { "package_name": "lazy_static", "package_version": "1.5.0", @@ -1067,18 +1043,6 @@ "repository": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide", "license": "MIT OR Zlib OR Apache-2.0" }, - { - "package_name": "ml-dsa", - "package_version": "0.1.1", - "repository": "https://github.com/RustCrypto/signatures", - "license": "Apache-2.0 OR MIT" - }, - { - "package_name": "module-lattice", - "package_version": "0.2.3", - "repository": "https://github.com/RustCrypto/KEMs", - "license": "Apache-2.0 OR MIT" - }, { "package_name": "moxcms", "package_version": "0.7.11", @@ -1433,6 +1397,12 @@ "repository": "https://github.com/rust-random/rand", "license": "MIT OR Apache-2.0" }, + { + "package_name": "rand", + "package_version": "0.10.2", + "repository": "https://github.com/rust-random/rand", + "license": "MIT OR Apache-2.0" + }, { "package_name": "rand_chacha", "package_version": "0.3.1", @@ -1613,12 +1583,6 @@ "repository": "https://github.com/serde-rs/serde", "license": "MIT OR Apache-2.0" }, - { - "package_name": "serde_json", - "package_version": "1.0.151", - "repository": "https://github.com/serde-rs/json", - "license": "MIT OR Apache-2.0" - }, { "package_name": "serde_spanned", "package_version": "0.6.9", @@ -1655,12 +1619,6 @@ "repository": "https://github.com/RustCrypto/hashes", "license": "MIT OR Apache-2.0" }, - { - "package_name": "shake", - "package_version": "0.1.0", - "repository": "https://github.com/RustCrypto/XOFs", - "license": "MIT OR Apache-2.0" - }, { "package_name": "signature", "package_version": "2.2.0", @@ -1745,12 +1703,6 @@ "repository": "https://github.com/RustCrypto/formats", "license": "Apache-2.0 OR MIT" }, - { - "package_name": "sponge-cursor", - "package_version": "0.1.0", - "repository": "https://github.com/RustCrypto/utils", - "license": "MIT OR Apache-2.0" - }, { "package_name": "subtle", "package_version": "2.6.1", @@ -2075,12 +2027,6 @@ "repository": "https://github.com/trifectatechfoundation/zlib-rs", "license": "Zlib" }, - { - "package_name": "zmij", - "package_version": "1.0.23", - "repository": "https://github.com/dtolnay/zmij", - "license": "MIT" - }, { "package_name": "zune-core", "package_version": "0.4.12", diff --git a/rust/src/api/piv_crypto.rs b/rust/src/api/piv_crypto.rs index eda87f4..b8fd5cf 100644 --- a/rust/src/api/piv_crypto.rs +++ b/rust/src/api/piv_crypto.rs @@ -263,24 +263,53 @@ pub fn piv_certificate_supports_macos( slot: u8, now_unix: i64, ) -> bool { - let Ok((remaining, cert)) = x509_parser::parse_x509_certificate(&der) else { return false; }; - if !remaining.is_empty() || cert.public_key().raw != expected_public_key { return false; } - let Ok(now) = x509_parser::time::ASN1Time::from_timestamp(now_unix) else { return false; }; - if !cert.validity().is_valid_at(now) { return false; } - let Ok(basic) = cert.basic_constraints() else { return false; }; - if basic.is_some_and(|ext| ext.value.ca) { return false; } - let Ok(usage) = cert.key_usage() else { return false; }; - let Ok(eku) = cert.extended_key_usage() else { return false; }; - let Ok(spki) = SubjectPublicKeyInfo::from_der(&expected_public_key) else { return false; }; - let Ok(algorithm) = algorithm_from_spki(&spki) else { return false; }; - if algorithm != PIV_ECC_P256 && algorithm != PIV_RSA2048 { return false; } + let Ok((remaining, cert)) = x509_parser::parse_x509_certificate(&der) else { + return false; + }; + if !remaining.is_empty() || cert.public_key().raw != expected_public_key { + return false; + } + let Ok(now) = x509_parser::time::ASN1Time::from_timestamp(now_unix) else { + return false; + }; + if !cert.validity().is_valid_at(now) { + return false; + } + let Ok(basic) = cert.basic_constraints() else { + return false; + }; + if basic.is_some_and(|ext| ext.value.ca) { + return false; + } + let Ok(usage) = cert.key_usage() else { + return false; + }; + let Ok(eku) = cert.extended_key_usage() else { + return false; + }; + let Ok(spki) = SubjectPublicKeyInfo::from_der(&expected_public_key) else { + return false; + }; + let Ok(algorithm) = algorithm_from_spki(&spki) else { + return false; + }; + if algorithm != PIV_ECC_P256 && algorithm != PIV_RSA2048 { + return false; + } match slot { - 0x9A => usage.is_none_or(|ext| ext.value.digital_signature()) - && eku.is_none_or(|ext| ext.value.any || ext.value.client_auth), - 0x9D => usage.is_none_or(|ext| if algorithm == PIV_RSA2048 { - ext.value.key_encipherment() - } else { ext.value.key_agreement() }) - && eku.is_none_or(|ext| ext.value.any), + 0x9A => { + usage.is_none_or(|ext| ext.value.digital_signature()) + && eku.is_none_or(|ext| ext.value.any || ext.value.client_auth) + } + 0x9D => { + usage.is_none_or(|ext| { + if algorithm == PIV_RSA2048 { + ext.value.key_encipherment() + } else { + ext.value.key_agreement() + } + }) && eku.is_none_or(|ext| ext.value.any) + } _ => false, } } @@ -946,10 +975,8 @@ mod tests { metadata.subject_public_key_info, response.subject_public_key_info ); - let key = rsa::RsaPublicKey::from_public_key_der( - &metadata.subject_public_key_info, - ) - .unwrap(); + let key = + rsa::RsaPublicKey::from_public_key_der(&metadata.subject_public_key_info).unwrap(); assert_eq!(key.n().to_bytes_be(), modulus); assert_eq!(key.e().to_bytes_be(), vec![1, 0, 1]); } @@ -967,13 +994,9 @@ mod tests { assert!(error.contains(&rsa::errors::Error::InvalidModulus.to_string())); assert!(error.contains("modulus: 3 bytes, exponent: 3 bytes")); - let error = build_piv_public_key( - PIV_RSA2048, - vec![0x81, 3, 0x80, 0, 1, 0x82, 1, 2], - false, - ) - .err() - .unwrap(); + let error = build_piv_public_key(PIV_RSA2048, vec![0x81, 3, 0x80, 0, 1, 0x82, 1, 2], false) + .err() + .unwrap(); assert!(error.contains(&rsa::errors::Error::InvalidExponent.to_string())); } @@ -1148,20 +1171,38 @@ mod tests { fn macos_capability_requires_wrapping_usage_and_matching_valid_key() { let now = 1788912000; for (usage, eku, slot, expected) in [ - (1, true, 0x9A, true), (1, true, 0x9D, false), - (16, false, 0x9D, true), (4, false, 0x9D, false), - (16, true, 0x9D, false), (0, false, 0x9D, true), + (1, true, 0x9A, true), + (1, true, 0x9D, false), + (16, false, 0x9D, true), + (4, false, 0x9D, false), + (16, true, 0x9D, false), + (0, false, 0x9D, true), (16, false, 0x9A, false), ] { let mut params = extension_test_params(); params.key_usage = usage; - if !eku { params.extended_key_usage.clear(); } + if !eku { + params.extended_key_usage.clear(); + } let spki = params.subject_public_key_info.clone(); let tbs = prepare_self_signed_certificate(params).unwrap(); let cert = finish_self_signed_certificate(tbs, PIV_ECC_P256, vec![1; 64]).unwrap(); - assert_eq!(piv_certificate_supports_macos(cert.clone(), spki.clone(), slot, now), expected); - assert!(!piv_certificate_supports_macos(cert.clone(), vec![], slot, now)); - assert!(!piv_certificate_supports_macos(cert.clone(), spki.clone(), slot, 1900000000)); + assert_eq!( + piv_certificate_supports_macos(cert.clone(), spki.clone(), slot, now), + expected + ); + assert!(!piv_certificate_supports_macos( + cert.clone(), + vec![], + slot, + now + )); + assert!(!piv_certificate_supports_macos( + cert.clone(), + spki.clone(), + slot, + 1900000000 + )); let mut trailing = cert; trailing.push(0); assert!(!piv_certificate_supports_macos(trailing, spki, slot, now)); @@ -1266,7 +1307,12 @@ mod tests { }; let certificate = finish_self_signed_certificate(tbs, algorithm, vec![1; signature_len]).unwrap(); - assert!(piv_certificate_supports_macos(certificate.clone(), spki, 0x9D, 1788912000)); + assert!(piv_certificate_supports_macos( + certificate.clone(), + spki, + 0x9D, + 1788912000 + )); assert!(parse_x509_cert_from_der(certificate).is_ok()); } } diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs index e0d0a36..5995e77 100644 --- a/rust/src/api/protocol.rs +++ b/rust/src/api/protocol.rs @@ -190,6 +190,9 @@ fn pass_slots_data(slots: admin::PassSlots) -> Vec { } /// Structured, payload-free protocol failure. Transport errors remain in Dart. +/// For CTAP-level failures `status_word` carries the raw CTAP status byte +/// widened to u16 (for example 0x0031 for PIN_INVALID), NOT an ISO 7816 +/// status word; see canokey-ctap's status table. pub struct ProtocolError { pub kind: String, pub phase: String, @@ -218,6 +221,8 @@ pub struct ProtocolStep { pub error: Option, pub profile: Option, pub admin: Option, + pub pin_session: Option, + pub pin_token: Option, } impl ProtocolStep { @@ -228,6 +233,8 @@ impl ProtocolStep { error: Some(error.into()), profile: None, admin: None, + pin_session: None, + pin_token: None, } } } @@ -253,6 +260,11 @@ enum Inner { NdefCapability(Operation), NdefMessage(Operation), Ctap(Operation), + CtapGetInfo(Operation), + CtapPinSession(Operation), + CtapPinToken(Operation, ctap::PinUvAuthProtocol), + CtapRps(Operation>), + CtapCredentials(Operation>), } /// Immutable observations of one device. Dart binds this owner to a card lease. @@ -2187,6 +2199,349 @@ fn piv_slot(reference: u8) -> Result { } } +/// CTAP getInfo fields Dart needs, parsed in Rust so Dart never touches CBOR: +/// `credMgmt | clientPin | forcePinChange` as tri-state bytes (0 = absent, +/// 1 = false, 2 = true), then a minPinLength flag byte (0 = absent; 1 = +/// present, followed by a big-endian u64), then `count | protocol bytes` +/// with the raw advertised pinUvAuthProtocol versions. +fn ctap_info_data(info: &ctap::AuthenticatorInfo) -> Vec { + fn tristate(value: Option) -> u8 { + match value { + None => 0, + Some(false) => 1, + Some(true) => 2, + } + } + let option = |name: &str| { + info.options() + .and_then(|options| options.iter().find(|(key, _)| key == name).map(|(_, v)| *v)) + }; + let mut out = vec![ + tristate(option("credMgmt")), + tristate(option("clientPin")), + tristate(info.force_pin_change()), + ]; + match info.min_pin_length() { + Some(length) => { + out.push(1); + out.extend_from_slice(&length.to_be_bytes()); + } + None => out.push(0), + } + let protocols = info.pin_uv_auth_protocols().unwrap_or(&[]); + out.push(protocols.len() as u8); + out.extend_from_slice(protocols); + out +} + +/// Self-delimiting RP entries: `id_len | id | name_len (0xff = absent) | +/// name | rp_id_hash[32]`, concatenated. Text fields are length-prefixed so +/// Dart never parses CBOR. +fn ctap_rps_data(entries: Vec) -> Vec { + let mut out = Vec::new(); + for entry in entries { + out.push(entry.rp.id.len() as u8); + out.extend_from_slice(entry.rp.id.as_bytes()); + match &entry.rp.name { + Some(name) => { + out.push(name.len() as u8); + out.extend_from_slice(name.as_bytes()); + } + None => out.push(0xff), + } + out.extend_from_slice(&entry.rp_id_hash); + } + out +} + +/// Self-delimiting credential entries, concatenated: +/// `cred_id_len (u16 BE) | cred_id | user_flag (0/1)`, and when present +/// `user_id_len | user_id | name_len (0xff absent) | name | display_len +/// (0xff absent) | display`; then `cred_protect (0xff absent, else the +/// value)`; then `key_form`: 0x01 metadataOnly with the raw COSE algorithm +/// (i64 BE, CanoKey extension key 0x80), or 0x02 publicKey with the resolved +/// COSE algorithm (i64 BE, i64::MIN when unresolvable) followed by +/// `key_len (u16 BE) | canonical CBOR COSE key` (opaque pass-through). +fn ctap_credentials_data(entries: Vec) -> Vec { + let mut out = Vec::new(); + for entry in entries { + let id = &entry.credential_id.id; + out.extend_from_slice(&(id.len() as u16).to_be_bytes()); + out.extend_from_slice(id); + match &entry.user { + Some(user) => { + out.push(1); + out.push(user.id.len() as u8); + out.extend_from_slice(&user.id); + for text in [&user.name, &user.display_name] { + match text { + Some(text) => { + out.push(text.len() as u8); + out.extend_from_slice(text.as_bytes()); + } + None => out.push(0xff), + } + } + } + None => out.push(0), + } + match entry.cred_protect { + Some(policy) => out.push(policy as u8), + None => out.push(0xff), + } + match (&entry.cose_algorithm, &entry.public_key) { + (Some(algorithm), None) => { + out.push(0x01); + out.extend_from_slice(&algorithm.to_be_bytes()); + } + (None, Some(key)) => { + out.push(0x02); + let algorithm = key.algorithm().map(|a| a.id()).unwrap_or(i64::MIN); + out.extend_from_slice(&algorithm.to_be_bytes()); + let encoded = ctap::cbor::encode(&key.to_value()); + out.extend_from_slice(&(encoded.len() as u16).to_be_bytes()); + out.extend_from_slice(&encoded); + } + // The upstream parser rejects both/neither before this point. + _ => unreachable!("credential entries carry exactly one key form"), + } + } + out +} + +/// Fresh V2 IV (16 CSPRNG bytes); protocol V1 uses the spec-mandated zero +/// IV, which the upstream API expresses as `None`. +fn pin_iv(protocol: ctap::PinUvAuthProtocol) -> Option<[u8; 16]> { + if protocol == ctap::PinUvAuthProtocol::V2 { + let mut iv = [0u8; 16]; + rand::fill(&mut iv); + Some(iv) + } else { + None + } +} + +/// A CTAP2 ClientPIN key-agreement session (P-256 ECDH). The shared secret +/// lives only in this zeroizing Rust owner and never crosses the bridge. +/// Dart holds the handle between operations; `close` is idempotent local +/// cleanup and sends nothing. +#[flutter_rust_bridge::frb(opaque)] +pub struct CtapPinSession { + inner: Option, + protocol: ctap::PinUvAuthProtocol, +} + +impl CtapPinSession { + fn new(session: ctap::PinSession) -> Self { + Self { + protocol: session.protocol(), + inner: Some(session), + } + } + + /// The pin/UV auth protocol version (1 or 2) this session speaks. + /// Dart picks it from getInfo's advertised pinUvAuthProtocols. + #[flutter_rust_bridge::frb(sync)] + pub fn protocol_version(&self) -> u8 { + self.protocol.to_u8() + } + + /// Set the initial PIN (subcommand 0x03). V2 IVs are generated here from + /// the CSPRNG; PIN bytes enter upstream zeroizing owners immediately. + #[flutter_rust_bridge::frb(sync)] + pub fn set_pin(&self, new_pin: Vec) -> ProtocolOperation { + self.set_pin_with_iv(SecretBytes::new(new_pin), pin_iv(self.protocol)) + } + + fn set_pin_with_iv(&self, new_pin: SecretBytes, iv: Option<[u8; 16]>) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let session = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + ctap::set_pin( + session, + new_pin.as_bytes(), + iv.as_ref(), + OperationOptions::default(), + ) + .map(Inner::Management) + })()) + } + + /// Change the PIN (subcommand 0x04); IV handling matches [`Self::set_pin`]. + #[flutter_rust_bridge::frb(sync)] + pub fn change_pin(&self, old_pin: Vec, new_pin: Vec) -> ProtocolOperation { + self.change_pin_with_iv( + SecretBytes::new(old_pin), + SecretBytes::new(new_pin), + pin_iv(self.protocol), + ) + } + + fn change_pin_with_iv( + &self, + old_pin: SecretBytes, + new_pin: SecretBytes, + iv: Option<[u8; 16]>, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let session = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + ctap::change_pin( + session, + old_pin.as_bytes(), + new_pin.as_bytes(), + iv.as_ref(), + OperationOptions::default(), + ) + .map(Inner::Management) + })()) + } + + /// Obtain a pinUvAuthToken with explicit permissions (subcommand 0x09): + /// `permissions` is the raw bitfield (credentialManagement = 0x04) and + /// `rp_id`, when present, binds the token to that relying party. The + /// decrypted token is returned as an opaque handle in the final step. + #[flutter_rust_bridge::frb(sync)] + pub fn get_pin_token_with_permissions( + &self, + pin: Vec, + permissions: u8, + rp_id: Option, + ) -> ProtocolOperation { + self.pin_token_with_iv( + SecretBytes::new(pin), + permissions, + rp_id, + pin_iv(self.protocol), + ) + } + + fn pin_token_with_iv( + &self, + pin: SecretBytes, + permissions: u8, + rp_id: Option, + iv: Option<[u8; 16]>, + ) -> ProtocolOperation { + let protocol = self.protocol; + ProtocolOperation::from_operation((|| { + let session = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + ctap::get_pin_token_with_permissions( + session, + pin.as_bytes(), + ctap::Permissions::from_bits(permissions), + rp_id.as_deref(), + iv.as_ref(), + OperationOptions::default(), + ) + .map(|operation| Inner::CtapPinToken(operation, protocol)) + })()) + } + + /// Idempotent local cleanup. Does not alter the card or pending operations. + #[flutter_rust_bridge::frb(sync)] + pub fn close(&mut self) { + self.inner.take(); + } +} + +/// A decrypted pinUvAuthToken held only in zeroizing Rust memory; the raw +/// token bytes never cross the bridge. Per CTAP 2.1 tokens are +/// ceremony-scoped: obtain a fresh one per use instead of caching the handle. +#[flutter_rust_bridge::frb(opaque)] +pub struct CtapPinToken { + inner: Option, + protocol: ctap::PinUvAuthProtocol, +} + +impl CtapPinToken { + fn new(token: ctap::PinToken, protocol: ctap::PinUvAuthProtocol) -> Self { + Self { + inner: Some(token), + protocol, + } + } + + /// The pin/UV auth protocol version (1 or 2) of the session that minted + /// this token; credmgmt operations authenticate with it. + #[flutter_rust_bridge::frb(sync)] + pub fn protocol_version(&self) -> u8 { + self.protocol.to_u8() + } + + fn token(&self) -> Result<&ctap::PinToken, Error> { + self.inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError)) + } + + /// Enumerate relying parties with resident credentials (Begin + GetNext + /// run inside the one operation). A 0x2E NO_CREDENTIALS status yields an + /// empty result, not an error. Data: see `ctap_rps_data`. + #[flutter_rust_bridge::frb(sync)] + pub fn enumerate_rps(&self) -> ProtocolOperation { + ProtocolOperation::from_operation(self.token().and_then(|token| { + ctap::credmgmt::enumerate_rps(token, self.protocol, OperationOptions::default()) + .map(Inner::CtapRps) + })) + } + + /// Enumerate one RP's resident credentials. `metadata_only` enables the + /// CanoKey vendor extension (subCommandParams key 0x80): the raw COSE + /// algorithm replaces the public key in responses. Data: see + /// `ctap_credentials_data`. A Begin 0x2E yields an empty result. + #[flutter_rust_bridge::frb(sync)] + pub fn enumerate_credentials( + &self, + rp_id_hash: Vec, + metadata_only: bool, + ) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let hash: [u8; 32] = rp_id_hash + .try_into() + .map_err(|_| Error::new(ErrorKind::InvalidArgument))?; + ctap::credmgmt::enumerate_credentials( + self.token()?, + self.protocol, + hash, + metadata_only, + OperationOptions::default(), + ) + .map(Inner::CtapCredentials) + })()) + } + + /// Permanently delete one resident credential by its raw credential ID + /// (sent as a `public-key` descriptor). Never retried or rolled back; + /// an unknown ID surfaces as NotFound (CTAP 0x2E/0x22). + #[flutter_rust_bridge::frb(sync)] + pub fn delete_credential(&self, credential_id: Vec) -> ProtocolOperation { + ProtocolOperation::from_operation((|| { + let descriptor = ctap::PublicKeyCredentialDescriptor::new("public-key", credential_id); + ctap::credmgmt::delete_credential( + self.token()?, + self.protocol, + &descriptor, + OperationOptions::default(), + ) + .map(Inner::Management) + })()) + } + + /// Idempotent local cleanup. Does not alter the card or pending operations. + #[flutter_rust_bridge::frb(sync)] + pub fn close(&mut self) { + self.inner.take(); + } +} + // The pinned high-level get_pin_status inserts SELECT. This selected-only // adapter uses its upstream command and error classifier, preserving live PIN // verification. It returns only the validated status word, never response data. @@ -2325,6 +2680,42 @@ impl ProtocolOperation { ) } + /// authenticatorGetInfo (0x04), profile-free with its own SELECT. The + /// final `data` carries the parsed fields Dart needs (credMgmt/clientPin + /// tri-states, forcePinChange, minPinLength, advertised pinUvAuthProtocol + /// versions); see `ctap_info_data` for the encoding. + #[flutter_rust_bridge::frb(sync)] + pub fn ctap_get_info() -> Self { + Self::from_operation(ctap::get_info(OperationOptions::default()).map(Inner::CtapGetInfo)) + } + + /// Start ClientPIN key agreement (subcommand 0x02) for protocol version + /// 1 or 2, generating the ephemeral P-256 scalar from the CSPRNG. The + /// final step carries an opaque [`CtapPinSession`] handle. + #[flutter_rust_bridge::frb(sync)] + pub fn ctap_begin_pin_session(protocol: u8) -> Self { + let operation = (|| { + let protocol = ctap::PinUvAuthProtocol::from_u8(protocol) + .ok_or_else(|| Error::new(ErrorKind::InvalidArgument))?; + // A random scalar that is zero or out of range is rejected by the + // upstream factory before any I/O; simply draw again. + let mut last_error = None; + for _ in 0..4 { + let mut scalar = [0u8; 32]; + rand::fill(&mut scalar); + match ctap::get_key_agreement(protocol, &scalar, OperationOptions::default()) { + Ok(operation) => return Ok(operation), + Err(error) if error.kind == ErrorKind::InvalidArgument => { + last_error = Some(error) + } + Err(error) => return Err(error), + } + } + Err(last_error.unwrap_or_else(|| Error::new(ErrorKind::InvalidArgument))) + })(); + Self::from_operation(operation.map(Inner::CtapPinSession)) + } + fn from_operation(operation: Result) -> Self { match operation { Ok(inner) => Self { @@ -2380,6 +2771,8 @@ impl ProtocolOperation { profile: None, error: None, admin: Some(admin_result(outcome)), + pin_session: None, + pin_token: None, }), Some(Inner::Bytes(op)) => drive(op, response, |data| bytes(data.as_bytes().to_vec())), Some(Inner::PinStatus(op)) => drive(op, response, |status| { @@ -2496,6 +2889,8 @@ impl ProtocolOperation { profile: Some(ProtocolProfile { inner: Some(profile), }), + pin_session: None, + pin_token: None, }), Some(Inner::NdefCapability(op)) => drive(op, response, |capability| { let max = capability.max_message_length.min(usize::from(u16::MAX)) as u16; @@ -2512,6 +2907,36 @@ impl ProtocolOperation { data.extend_from_slice(result.payload()); bytes(data) }), + Some(Inner::CtapGetInfo(op)) => { + drive(op, response, |info| bytes(ctap_info_data(&info))) + } + Some(Inner::CtapPinSession(op)) => drive(op, response, |session| ProtocolStep { + command: None, + data: None, + error: None, + profile: None, + admin: None, + pin_session: Some(CtapPinSession::new(session)), + pin_token: None, + }), + Some(Inner::CtapPinToken(op, protocol)) => { + let protocol = *protocol; + drive(op, response, |token| ProtocolStep { + command: None, + data: None, + error: None, + profile: None, + admin: None, + pin_session: None, + pin_token: Some(CtapPinToken::new(token, protocol)), + }) + } + Some(Inner::CtapRps(op)) => { + drive(op, response, |entries| bytes(ctap_rps_data(entries))) + } + Some(Inner::CtapCredentials(op)) => drive(op, response, |entries| { + bytes(ctap_credentials_data(entries)) + }), None => Err(Error::new(ErrorKind::OperationStateError)), }; result.unwrap_or_else(ProtocolStep::failure) @@ -2552,6 +2977,8 @@ fn drive( error: None, profile: None, admin: None, + pin_session: None, + pin_token: None, }, Step::Done => encode(op.take_result()?), }) @@ -2564,6 +2991,8 @@ fn bytes(data: Vec) -> ProtocolStep { error: None, profile: None, admin: None, + pin_session: None, + pin_token: None, } } @@ -3798,4 +4227,439 @@ mod tests { let mut op = ProtocolOperation::ctap_transceive(vec![]); assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); } + + // ---- CTAP2 client-layer bindings (getInfo/ClientPIN/credmgmt) -------- + // + // Golden fixtures are the upstream canokey-ctap known answers + // (crates/canokey-ctap/tests/client_pin.rs, credmgmt.rs): ephemeral + // scalar 0x01..=0x20, peer scalar 0xA0..=0xBF, PINs "1234"/"654321", + // V2 IV 0F..00, token plaintext 0x10..=0x2F. + + const CTAP_SELECT: [u8; 13] = [0, 0xa4, 4, 0, 8, 0xa0, 0, 0, 6, 0x47, 0x2f, 0, 1]; + const CTAP_EPHEMERAL_SCALAR: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, + ]; + const CTAP_IV_V2: [u8; 16] = [ + 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + 0x00, + ]; + const CTAP_PEER_KEY_AGREEMENT: &str = "a101a5010203381820012158200d0918a04198474605615b6df90fdcb34791fb3ecb822f4b26eb6e4fc4511b9d22582019b90c1b83c0c35cfbbb31ead32bb52ae33622f57e3cc1638097ce97f430baba"; + const CTAP_SET_PIN_V1: &str = "06a50101020303a501020338182001215820515c3d6eb9e396b904d3feca7f54fdcd0cc1e997bf375dca515ad0a6c3b4035f2258204536be3a50f318fbf9a5475902a221502bef0d57e08c53b2cc0a56f17d9f93540450d4421839fa7606bcd586eddc6494d9620558406ce3b2501aea6143416b8de3b22671a0a351bc7cd39c13236e20a0c9e999c7da30a83a78c0ebe15c1948375d871d23c2fc6b34915a52f806f4be73f9c604f056"; + const CTAP_CHANGE_PIN_V2: &str = "06a60102020403a501020338182001215820515c3d6eb9e396b904d3feca7f54fdcd0cc1e997bf375dca515ad0a6c3b4035f2258204536be3a50f318fbf9a5475902a221502bef0d57e08c53b2cc0a56f17d9f93540458206c2bb83d6ccfae4ce1221fa074d85cd5fae42f9ab7f65dd8c09ac8616429245b0558500f0e0d0c0b0a09080706050403020100a8395df2289b10495a9c1c13919cfdda5971cd6436294c7bafd204cd4eb1c737bbb21695023d71532c8cd847780477e0bf741b09ac0d72fdbc21ac401fe3ff1b0658200f0e0d0c0b0a0908070605040302010017d937071b239273a39da474e04c6376"; + const CTAP_GET_TOKEN_PERMS_V2: &str = "06a60102020903a501020338182001215820515c3d6eb9e396b904d3feca7f54fdcd0cc1e997bf375dca515ad0a6c3b4035f2258204536be3a50f318fbf9a5475902a221502bef0d57e08c53b2cc0a56f17d9f93540658200f0e0d0c0b0a0908070605040302010017d937071b239273a39da474e04c637609030a6b6578616d706c652e636f6d"; + const CTAP_TOKEN_CT_V1: &str = + "b98cc635132fa3ea8c191b7a4aa3e093ce926c35488221b4684fce766f3b14b0"; + const CTAP_TOKEN_CT_V2: &str = "0f0e0d0c0b0a09080706050403020100a1f914f091032bf439a162dbf45e137290ccdf4a4476a5f39b912b7dbbbb6f64"; + const CTAP_MSG_RPS_BEGIN: &str = "0aa3010203010450d5dc1a03a1a284cf096b59a579eaf833"; + const CTAP_MSG_RPS_NEXT: &str = "0aa201030301"; + const CTAP_RESP_RP_BEGIN: &str = "a303a26269646b6578616d706c652e636f6d646e616d65674578616d706c65045820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf0502"; + const CTAP_RESP_RP_NEXT: &str = "a203a1626964696f746865722e6f7267045820c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf"; + const CTAP_MSG_CREDS_META: &str = "0aa4010402a2015820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf1880f5030104503ffd7ae91f8e3630cd70f9f245edec40"; + const CTAP_MSG_CREDS_NEXT: &str = "0aa201050301"; + const CTAP_RESP_CRED_META: &str = "a406a26269644405060708646e616d6565616c69636507a2626964440102030464747970656a7075626c69632d6b6579090118803830"; + const CTAP_RESP_CRED_BEGIN: &str = "a606a36269644405060708646e616d6565616c6963656b646973706c61794e616d6565416c69636507a2626964440102030464747970656a7075626c69632d6b657908a50102032620012158201111111111111111111111111111111111111111111111111111111111111111225820222222222222222222222222222222222222222222222222222222222222222209020a020b58203333333333333333333333333333333333333333333333333333333333333333"; + const CTAP_RESP_CRED_NEXT: &str = "a207a2626964440908070664747970656a7075626c69632d6b657908a501020326200121582044444444444444444444444444444444444444444444444444444444444444442258205555555555555555555555555555555555555555555555555555555555555555"; + const CTAP_MSG_DELETE: &str = "0aa4010602a102a2626964440102030464747970656a7075626c69632d6b657903010450038bc0ba9fef742b26ce891514a2142c"; + const CTAP_RP_ID_HASH: [u8; 32] = [ + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, + 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, + 0xbe, 0xbf, + ]; + const CTAP_RP2_ID_HASH: [u8; 32] = [ + 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, + 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, + 0xde, 0xdf, + ]; + + fn ctap_hex(s: &str) -> Vec { + let clean: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + (0..clean.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&clean[i..i + 2], 16).unwrap()) + .collect() + } + + /// The APDU wrapping the envelope emits for a CTAP message. + fn ctap_wrapped(message: &[u8]) -> Vec { + let mut expected = vec![0x80, 0x10, 0x00, 0x00]; + if message.len() <= 255 { + expected.push(message.len() as u8); + } else { + expected.push(0); + expected.extend_from_slice(&(message.len() as u16).to_be_bytes()); + } + expected.extend_from_slice(message); + expected + } + + /// A successful CTAP response (status 0x00) carrying `payload`. + fn ctap_ok(payload: &[u8]) -> Vec { + let mut reply = vec![0x00]; + reply.extend_from_slice(payload); + reply.extend_from_slice(&[0x90, 0x00]); + reply + } + + /// Drive a facade operation through SELECT and return the next step. + fn ctap_after_select(op: &mut ProtocolOperation) -> ProtocolStep { + assert_eq!(op.start().command.unwrap(), CTAP_SELECT); + op.advance(vec![0x90, 0]) + } + + /// A deterministic fixture session built at the upstream API level. + fn ctap_fixture_session(protocol: ctap::PinUvAuthProtocol) -> CtapPinSession { + let mut op = ctap::get_key_agreement( + protocol, + &CTAP_EPHEMERAL_SCALAR, + OperationOptions::default(), + ) + .unwrap(); + assert_eq!(op.start().unwrap(), Step::Exchange); + assert_eq!(op.advance(&[0x90, 0]).unwrap(), Step::Exchange); + assert_eq!( + op.advance(&ctap_ok(&ctap_hex(CTAP_PEER_KEY_AGREEMENT))) + .unwrap(), + Step::Done + ); + CtapPinSession::new(op.take_result().unwrap()) + } + + /// The fixed V1 token (plaintext 0x10..=0x2F) via the fixture transcript. + fn ctap_fixture_token_v1() -> CtapPinToken { + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + let mut op = ctap::get_pin_token( + session.inner.as_ref().unwrap(), + b"1234", + None, + OperationOptions::default(), + ) + .unwrap(); + assert_eq!(op.start().unwrap(), Step::Exchange); + assert_eq!(op.advance(&[0x90, 0]).unwrap(), Step::Exchange); + let mut payload = vec![0xa1, 0x02, 0x58, 0x20]; + payload.extend_from_slice(&ctap_hex(CTAP_TOKEN_CT_V1)); + assert_eq!(op.advance(&ctap_ok(&payload)).unwrap(), Step::Done); + CtapPinToken::new(op.take_result().unwrap(), ctap::PinUvAuthProtocol::V1) + } + + /// getInfo fixture: versions [FIDO_2_0, FIDO_2_1], credMgmt true, + /// clientPin false, forcePinChange true, minPinLength 4, protocols [1, 2]. + fn ctap_get_info_payload() -> Vec { + ctap_hex( + "a6 \ + 01 82 684649444f5f325f30 684649444f5f325f31 \ + 03 50 244eb29ee0904e4981fe1f20f8d3b8f4 \ + 04 a3 62726bf5 68637265644d676d74f5 69636c69656e7450696ef4 \ + 06 82 01 02 \ + 0c f5 \ + 0d 04", + ) + } + + #[test] + fn ctap_get_info_parses_fields_and_keeps_ctap_status() { + let mut op = ProtocolOperation::ctap_get_info(); + let step = ctap_after_select(&mut op); + assert_eq!(step.command.unwrap(), ctap_wrapped(&[0x04])); + let data = op.advance(ctap_ok(&ctap_get_info_payload())).data.unwrap(); + assert_eq!( + data, + [ + 2, 1, 2, // credMgmt true, clientPin false, forcePinChange true + 1, 0, 0, 0, 0, 0, 0, 0, 4, // minPinLength 4 (u64 BE) + 2, 1, 2, // pinUvAuthProtocols [1, 2] + ] + ); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + + // A tri-state stays absent when the option is unadvertised: minimal + // map with only versions and aaguid. + let mut op = ProtocolOperation::ctap_get_info(); + ctap_after_select(&mut op); + let minimal = + ctap_hex("a2 01 81 684649444f5f325f30 03 50 244eb29ee0904e4981fe1f20f8d3b8f4"); + let data = op.advance(ctap_ok(&minimal)).data.unwrap(); + assert_eq!(data, [0, 0, 0, 0, 0]); + + // A CTAP-level failure keeps the raw CTAP status byte, not an ISO SW. + let mut op = ProtocolOperation::ctap_get_info(); + ctap_after_select(&mut op); + let error = op.advance(vec![0x30, 0x90, 0x00]).error.unwrap(); + assert_eq!(error.kind, "ConditionsNotSatisfied"); + assert_eq!(error.phase, "Command"); + assert_eq!(error.status_word, Some(0x30)); + + // A response missing the required versions key is malformed. + let mut op = ProtocolOperation::ctap_get_info(); + ctap_after_select(&mut op); + let bad = ctap_hex("a1 03 50 244eb29ee0904e4981fe1f20f8d3b8f4"); + let error = op.advance(ctap_ok(&bad)).error.unwrap(); + assert_eq!(error.kind, "InvalidResponse"); + assert_eq!(error.phase, "Parsing"); + } + + #[test] + fn ctap_begin_pin_session_drives_handshake_and_validates_protocol() { + let mut op = ProtocolOperation::ctap_begin_pin_session(1); + let step = ctap_after_select(&mut op); + // The ephemeral scalar is random, but the message shape is fixed. + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex("06 a2 01 01 02 02")) + ); + let step = op.advance(ctap_ok(&ctap_hex(CTAP_PEER_KEY_AGREEMENT))); + let session = step.pin_session.unwrap(); + assert_eq!(session.protocol_version(), 1); + + let mut op = ProtocolOperation::ctap_begin_pin_session(2); + ctap_after_select(&mut op); + let session = op + .advance(ctap_ok(&ctap_hex(CTAP_PEER_KEY_AGREEMENT))) + .pin_session + .unwrap(); + assert_eq!(session.protocol_version(), 2); + + // Unknown advertised protocol versions fail before any I/O. + let mut op = ProtocolOperation::ctap_begin_pin_session(3); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + } + + #[test] + fn ctap_set_and_change_pin_match_golden_wire_bytes() { + // V1 golden (upstream known answer, zero IV by specification). + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + let mut op = session.set_pin(b"1234".to_vec()); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_SET_PIN_V1)) + ); + assert_eq!( + op.advance(vec![0x00, 0x90, 0]).data.unwrap(), + Vec::::new() + ); + + // V2 golden with the fixture IV. + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V2); + let mut op = session.change_pin_with_iv( + SecretBytes::new(b"1234".to_vec()), + SecretBytes::new(b"654321".to_vec()), + Some(CTAP_IV_V2), + ); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_CHANGE_PIN_V2)) + ); + assert_eq!( + op.advance(vec![0x00, 0x90, 0]).data.unwrap(), + Vec::::new() + ); + + // PIN_INVALID keeps the raw CTAP status byte in status_word. + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + let mut op = session.change_pin(b"1234".to_vec(), b"654321".to_vec()); + ctap_after_select(&mut op); + let error = op.advance(vec![0x31, 0x90, 0x00]).error.unwrap(); + assert_eq!(error.kind, "InvalidPin"); + assert_eq!(error.status_word, Some(0x31)); + + // PIN validation happens before any I/O; a closed session is inert. + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + let mut op = session.set_pin(b"ab".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); + let mut session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + session.close(); + let mut op = session.set_pin(b"1234".to_vec()); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + } + + #[test] + fn ctap_v2_operations_generate_fresh_ivs_internally() { + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V2); + let mut first = session.set_pin(b"1234".to_vec()); + let mut second = session.set_pin(b"1234".to_vec()); + let first_command = ctap_after_select(&mut first).command.unwrap(); + let second_command = ctap_after_select(&mut second).command.unwrap(); + assert_ne!(first_command, second_command, "V2 IVs must be fresh"); + assert_eq!(first_command.len(), second_command.len()); + assert_eq!( + first.advance(vec![0x00, 0x90, 0]).data.unwrap(), + Vec::::new() + ); + } + + #[test] + fn ctap_pin_token_golden_and_ctap_error_mapping() { + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V2); + let mut op = session.pin_token_with_iv( + SecretBytes::new(b"1234".to_vec()), + 0x03, + Some("example.com".to_string()), + Some(CTAP_IV_V2), + ); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_GET_TOKEN_PERMS_V2)) + ); + let mut payload = vec![0xa1, 0x02, 0x58, 0x30]; + payload.extend_from_slice(&ctap_hex(CTAP_TOKEN_CT_V2)); + let token = op.advance(ctap_ok(&payload)).pin_token.unwrap(); + assert_eq!(token.protocol_version(), 2); + + // Zero permissions and PIN validation fail before any I/O. + let mut op = session.get_pin_token_with_permissions(b"1234".to_vec(), 0, None); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut op = session.get_pin_token_with_permissions(vec![0xff, 0xfe, 0x41, 0x42], 4, None); + assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); + + // PIN_BLOCKED keeps the raw CTAP status byte. + let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); + let mut op = session.get_pin_token_with_permissions(b"1234".to_vec(), 4, None); + ctap_after_select(&mut op); + let error = op.advance(vec![0x32, 0x90, 0x00]).error.unwrap(); + assert_eq!(error.kind, "PinBlocked"); + assert_eq!(error.status_word, Some(0x32)); + } + + #[test] + fn ctap_enumerate_rps_encodes_entries_and_empty_is_not_an_error() { + let token = ctap_fixture_token_v1(); + let mut op = token.enumerate_rps(); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_MSG_RPS_BEGIN)) + ); + let step = op.advance(ctap_ok(&ctap_hex(CTAP_RESP_RP_BEGIN))); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_MSG_RPS_NEXT)) + ); + let data = op + .advance(ctap_ok(&ctap_hex(CTAP_RESP_RP_NEXT))) + .data + .unwrap(); + let mut expected = vec![11]; + expected.extend_from_slice(b"example.com"); + expected.push(7); + expected.extend_from_slice(b"Example"); + expected.extend_from_slice(&CTAP_RP_ID_HASH); + expected.push(9); + expected.extend_from_slice(b"other.org"); + expected.push(0xff); // no rp name + expected.extend_from_slice(&CTAP_RP2_ID_HASH); + assert_eq!(data, expected); + + // CTAP2_ERR_NO_CREDENTIALS on Begin is an empty list, not an error. + let mut op = token.enumerate_rps(); + ctap_after_select(&mut op); + assert_eq!( + op.advance(vec![0x2e, 0x90, 0x00]).data.unwrap(), + Vec::::new() + ); + } + + #[test] + fn ctap_enumerate_credentials_metadata_only_and_standard_encoding() { + let token = ctap_fixture_token_v1(); + let mut op = token.enumerate_credentials(CTAP_RP_ID_HASH.to_vec(), true); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_MSG_CREDS_META)) + ); + let data = op + .advance(ctap_ok(&ctap_hex(CTAP_RESP_CRED_META))) + .data + .unwrap(); + let mut expected = vec![0, 4, 1, 2, 3, 4]; // credential ID + expected.push(1); // user present + expected.extend_from_slice(&[4, 5, 6, 7, 8]); // user handle + expected.push(5); + expected.extend_from_slice(b"alice"); + expected.push(0xff); // no display name + expected.push(0xff); // no credProtect + expected.push(0x01); // metadataOnly: raw COSE algorithm follows + expected.extend_from_slice(&(-49i64).to_be_bytes()); + assert_eq!(data, expected); + + // Standard mode carries the public key as opaque canonical CBOR. + let mut op = token.enumerate_credentials(CTAP_RP_ID_HASH.to_vec(), false); + ctap_after_select(&mut op); + let step = op.advance(ctap_ok(&ctap_hex(CTAP_RESP_CRED_BEGIN))); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_MSG_CREDS_NEXT)) + ); + let data = op + .advance(ctap_ok(&ctap_hex(CTAP_RESP_CRED_NEXT))) + .data + .unwrap(); + let mut expected = vec![0, 4, 1, 2, 3, 4]; + expected.push(1); + expected.extend_from_slice(&[4, 5, 6, 7, 8]); + expected.push(5); + expected.extend_from_slice(b"alice"); + expected.push(5); + expected.extend_from_slice(b"Alice"); + expected.push(2); // credProtect + expected.push(0x02); + expected.extend_from_slice(&(-7i64).to_be_bytes()); + let key0 = ctap_hex( + "a501020326200121582011111111111111111111111111111111111111111111111111111111111111112258202222222222222222222222222222222222222222222222222222222222222222", + ); + expected.extend_from_slice(&(key0.len() as u16).to_be_bytes()); + expected.extend_from_slice(&key0); + expected.extend_from_slice(&[0, 4, 9, 8, 7, 6]); // second credential ID + expected.push(0); // no user + expected.push(0xff); // no credProtect + expected.push(0x02); + expected.extend_from_slice(&(-7i64).to_be_bytes()); + let key1 = ctap_hex( + "a501020326200121582044444444444444444444444444444444444444444444444444444444444444442258205555555555555555555555555555555555555555555555555555555555555555", + ); + expected.extend_from_slice(&(key1.len() as u16).to_be_bytes()); + expected.extend_from_slice(&key1); + assert_eq!(data, expected); + + // A 31-byte RP ID hash fails before any I/O. + let mut op = token.enumerate_credentials(vec![0; 31], false); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + } + + #[test] + fn ctap_delete_credential_golden_and_handle_lifecycle() { + let token = ctap_fixture_token_v1(); + let mut op = token.delete_credential(vec![1, 2, 3, 4]); + let step = ctap_after_select(&mut op); + assert_eq!( + step.command.unwrap(), + ctap_wrapped(&ctap_hex(CTAP_MSG_DELETE)) + ); + assert_eq!( + op.advance(vec![0x00, 0x90, 0]).data.unwrap(), + Vec::::new() + ); + + // An unknown credential is NotFound with the raw CTAP status byte. + let mut op = token.delete_credential(vec![1, 2, 3, 4]); + ctap_after_select(&mut op); + let error = op.advance(vec![0x2e, 0x90, 0x00]).error.unwrap(); + assert_eq!(error.kind, "NotFound"); + assert_eq!(error.status_word, Some(0x2e)); + + // An empty credential ID fails before any I/O; a closed token handle + // rejects further operations. + let mut op = token.delete_credential(vec![]); + assert_eq!(op.start().error.unwrap().kind, "InvalidArgument"); + let mut token = ctap_fixture_token_v1(); + token.close(); + token.close(); + let mut op = token.enumerate_rps(); + assert_eq!(op.start().error.unwrap().kind, "OperationStateError"); + } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index f9190f4..a084531 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2116397449; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1993561274; // Section: executor @@ -48,6 +48,508 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__api__protocol__CtapPinSession_change_pin_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinSession_change_pin", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_old_pin = >::sse_decode(&mut deserializer); + let api_new_pin = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::CtapPinSession::change_pin( + &*api_that_guard, + api_old_pin, + api_new_pin, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinSession_close_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinSession_close", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::protocol::CtapPinSession::close(&mut *api_that_guard); + })?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinSession_get_pin_token_with_permissions_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinSession_get_pin_token_with_permissions", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_pin = >::sse_decode(&mut deserializer); + let api_permissions = ::sse_decode(&mut deserializer); + let api_rp_id = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::CtapPinSession::get_pin_token_with_permissions( + &*api_that_guard, + api_pin, + api_permissions, + api_rp_id, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinSession_protocol_version_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinSession_protocol_version", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::CtapPinSession::protocol_version(&*api_that_guard), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinSession_set_pin_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinSession_set_pin", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_new_pin = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::CtapPinSession::set_pin( + &*api_that_guard, + api_new_pin, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinToken_close_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinToken_close", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref_mut()), + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>({ + crate::api::protocol::CtapPinToken::close(&mut *api_that_guard); + })?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinToken_delete_credential_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinToken_delete_credential", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_credential_id = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::CtapPinToken::delete_credential( + &*api_that_guard, + api_credential_id, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinToken_enumerate_credentials_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinToken_enumerate_credentials", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_rp_id_hash = >::sse_decode(&mut deserializer); + let api_metadata_only = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Ok::<_, ()>(crate::api::protocol::CtapPinToken::enumerate_credentials( + &*api_that_guard, + api_rp_id_hash, + api_metadata_only, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinToken_enumerate_rps_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinToken_enumerate_rps", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::CtapPinToken::enumerate_rps( + &*api_that_guard, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__CtapPinToken_protocol_version_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CtapPinToken_protocol_version", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>(crate::api::protocol::CtapPinToken::protocol_version( + &*api_that_guard, + ))?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__protocol__ProtocolOperation_admin_progress_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -227,6 +729,68 @@ fn wire__crate__api__protocol__ProtocolOperation_close_impl( }, ) } +fn wire__crate__api__protocol__ProtocolOperation_ctap_begin_pin_session_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_ctap_begin_pin_session", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_protocol = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolOperation::ctap_begin_pin_session(api_protocol), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__protocol__ProtocolOperation_ctap_get_info_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolOperation_ctap_get_info", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = + Ok::<_, ()>(crate::api::protocol::ProtocolOperation::ctap_get_info())?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__protocol__ProtocolOperation_ctap_select_application_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -5032,6 +5596,12 @@ fn wire__crate__api__crypto__verify_piv_signature_impl( // Section: related_funcs +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); @@ -5041,6 +5611,26 @@ flutter_rust_bridge::frb_generated_moi_arc_impl_value!( // Section: dart2rust +impl SseDecode for CtapPinSession { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for CtapPinToken { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode for ProtocolOperation { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5061,6 +5651,26 @@ impl SseDecode for ProtocolProfile { } } +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for RustOpaqueMoi> { @@ -5275,6 +5885,28 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5488,12 +6120,16 @@ impl SseDecode for crate::api::protocol::ProtocolStep { let mut var_error = >::sse_decode(deserializer); let mut var_profile = >::sse_decode(deserializer); let mut var_admin = >::sse_decode(deserializer); + let mut var_pinSession = >::sse_decode(deserializer); + let mut var_pinToken = >::sse_decode(deserializer); return crate::api::protocol::ProtocolStep { command: var_command, data: var_data, error: var_error, profile: var_profile, admin: var_admin, + pin_session: var_pinSession, + pin_token: var_pinToken, }; } } @@ -5618,435 +6254,511 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 1 => wire__crate__api__protocol__ProtocolOperation_admin_progress_impl( + 1 => { + wire__crate__api__protocol__CtapPinSession_change_pin_impl(ptr, rust_vec_len, data_len) + } + 2 => wire__crate__api__protocol__CtapPinSession_close_impl(ptr, rust_vec_len, data_len), + 3 => wire__crate__api__protocol__CtapPinSession_get_pin_token_with_permissions_impl( + ptr, + rust_vec_len, + data_len, + ), + 4 => wire__crate__api__protocol__CtapPinSession_protocol_version_impl( + ptr, + rust_vec_len, + data_len, + ), + 5 => wire__crate__api__protocol__CtapPinSession_set_pin_impl(ptr, rust_vec_len, data_len), + 6 => wire__crate__api__protocol__CtapPinToken_close_impl(ptr, rust_vec_len, data_len), + 7 => wire__crate__api__protocol__CtapPinToken_delete_credential_impl( + ptr, + rust_vec_len, + data_len, + ), + 8 => wire__crate__api__protocol__CtapPinToken_enumerate_credentials_impl( + ptr, + rust_vec_len, + data_len, + ), + 9 => { + wire__crate__api__protocol__CtapPinToken_enumerate_rps_impl(ptr, rust_vec_len, data_len) + } + 10 => wire__crate__api__protocol__CtapPinToken_protocol_version_impl( + ptr, + rust_vec_len, + data_len, + ), + 11 => wire__crate__api__protocol__ProtocolOperation_admin_progress_impl( ptr, rust_vec_len, data_len, ), - 2 => { + 12 => { wire__crate__api__protocol__ProtocolOperation_advance_impl(ptr, rust_vec_len, data_len) } - 3 => wire__crate__api__protocol__ProtocolOperation_bootstrap_identity_impl( + 13 => wire__crate__api__protocol__ProtocolOperation_bootstrap_identity_impl( + ptr, + rust_vec_len, + data_len, + ), + 14 => wire__crate__api__protocol__ProtocolOperation_close_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__api__protocol__ProtocolOperation_ctap_begin_pin_session_impl( + ptr, + rust_vec_len, + data_len, + ), + 16 => wire__crate__api__protocol__ProtocolOperation_ctap_get_info_impl( ptr, rust_vec_len, data_len, ), - 4 => wire__crate__api__protocol__ProtocolOperation_close_impl(ptr, rust_vec_len, data_len), - 5 => wire__crate__api__protocol__ProtocolOperation_ctap_select_application_impl( + 17 => wire__crate__api__protocol__ProtocolOperation_ctap_select_application_impl( ptr, rust_vec_len, data_len, ), - 6 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_impl( + 18 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_impl( ptr, rust_vec_len, data_len, ), - 7 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_selected_impl( + 19 => wire__crate__api__protocol__ProtocolOperation_ctap_transceive_selected_impl( ptr, rust_vec_len, data_len, ), - 8 => wire__crate__api__protocol__ProtocolOperation_ndef_read_capability_impl( + 20 => wire__crate__api__protocol__ProtocolOperation_ndef_read_capability_impl( ptr, rust_vec_len, data_len, ), - 9 => wire__crate__api__protocol__ProtocolOperation_ndef_read_message_impl( + 21 => wire__crate__api__protocol__ProtocolOperation_ndef_read_message_impl( ptr, rust_vec_len, data_len, ), - 10 => wire__crate__api__protocol__ProtocolOperation_ndef_write_message_impl( + 22 => wire__crate__api__protocol__ProtocolOperation_ndef_write_message_impl( ptr, rust_vec_len, data_len, ), - 11 => { + 23 => { wire__crate__api__protocol__ProtocolOperation_piv_read_impl(ptr, rust_vec_len, data_len) } - 12 => wire__crate__api__protocol__ProtocolOperation_probe_admin_impl( + 24 => wire__crate__api__protocol__ProtocolOperation_probe_admin_impl( ptr, rust_vec_len, data_len, ), - 13 => wire__crate__api__protocol__ProtocolOperation_probe_piv_impl( + 25 => wire__crate__api__protocol__ProtocolOperation_probe_piv_impl( ptr, rust_vec_len, data_len, ), - 14 => wire__crate__api__protocol__ProtocolOperation_start_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__api__protocol__ProtocolProfile_admin_action_impl( + 26 => wire__crate__api__protocol__ProtocolOperation_start_impl(ptr, rust_vec_len, data_len), + 27 => wire__crate__api__protocol__ProtocolProfile_admin_action_impl( ptr, rust_vec_len, data_len, ), - 16 => wire__crate__api__protocol__ProtocolProfile_admin_configure_impl( + 28 => wire__crate__api__protocol__ProtocolProfile_admin_configure_impl( ptr, rust_vec_len, data_len, ), - 17 => wire__crate__api__protocol__ProtocolProfile_admin_pass_slots_impl( + 29 => wire__crate__api__protocol__ProtocolProfile_admin_pass_slots_impl( ptr, rust_vec_len, data_len, ), - 18 => { + 30 => { wire__crate__api__protocol__ProtocolProfile_admin_read_impl(ptr, rust_vec_len, data_len) } - 19 => wire__crate__api__protocol__ProtocolProfile_admin_set_pass_slot_impl( + 31 => wire__crate__api__protocol__ProtocolProfile_admin_set_pass_slot_impl( ptr, rust_vec_len, data_len, ), - 20 => wire__crate__api__protocol__ProtocolProfile_close_impl(ptr, rust_vec_len, data_len), - 21 => { + 32 => wire__crate__api__protocol__ProtocolProfile_close_impl(ptr, rust_vec_len, data_len), + 33 => { wire__crate__api__protocol__ProtocolProfile_firmware_impl(ptr, rust_vec_len, data_len) } - 22 => wire__crate__api__protocol__ProtocolProfile_model_impl(ptr, rust_vec_len, data_len), - 23 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_impl( + 34 => wire__crate__api__protocol__ProtocolProfile_model_impl(ptr, rust_vec_len, data_len), + 35 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_impl( ptr, rust_vec_len, data_len, ), - 24 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_all_impl( + 36 => wire__crate__api__protocol__ProtocolProfile_oath_calculate_all_impl( ptr, rust_vec_len, data_len, ), - 25 => wire__crate__api__protocol__ProtocolProfile_oath_clear_code_impl( + 37 => wire__crate__api__protocol__ProtocolProfile_oath_clear_code_impl( ptr, rust_vec_len, data_len, ), - 26 => wire__crate__api__protocol__ProtocolProfile_oath_delete_impl( + 38 => wire__crate__api__protocol__ProtocolProfile_oath_delete_impl( ptr, rust_vec_len, data_len, ), - 27 => { + 39 => { wire__crate__api__protocol__ProtocolProfile_oath_list_impl(ptr, rust_vec_len, data_len) } - 28 => { + 40 => { wire__crate__api__protocol__ProtocolProfile_oath_put_impl(ptr, rust_vec_len, data_len) } - 29 => wire__crate__api__protocol__ProtocolProfile_oath_rename_impl( + 41 => wire__crate__api__protocol__ProtocolProfile_oath_rename_impl( ptr, rust_vec_len, data_len, ), - 30 => wire__crate__api__protocol__ProtocolProfile_oath_select_impl( + 42 => wire__crate__api__protocol__ProtocolProfile_oath_select_impl( ptr, rust_vec_len, data_len, ), - 31 => wire__crate__api__protocol__ProtocolProfile_oath_set_code_impl( + 43 => wire__crate__api__protocol__ProtocolProfile_oath_set_code_impl( ptr, rust_vec_len, data_len, ), - 32 => wire__crate__api__protocol__ProtocolProfile_oath_set_default_impl( + 44 => wire__crate__api__protocol__ProtocolProfile_oath_set_default_impl( ptr, rust_vec_len, data_len, ), - 33 => wire__crate__api__protocol__ProtocolProfile_oath_validate_impl( + 45 => wire__crate__api__protocol__ProtocolProfile_oath_validate_impl( ptr, rust_vec_len, data_len, ), - 34 => wire__crate__api__protocol__ProtocolProfile_openpgp_activate_impl( + 46 => wire__crate__api__protocol__ProtocolProfile_openpgp_activate_impl( ptr, rust_vec_len, data_len, ), - 35 => wire__crate__api__protocol__ProtocolProfile_openpgp_change_password_impl( + 47 => wire__crate__api__protocol__ProtocolProfile_openpgp_change_password_impl( ptr, rust_vec_len, data_len, ), - 36 => wire__crate__api__protocol__ProtocolProfile_openpgp_generate_key_impl( + 48 => wire__crate__api__protocol__ProtocolProfile_openpgp_generate_key_impl( ptr, rust_vec_len, data_len, ), - 37 => wire__crate__api__protocol__ProtocolProfile_openpgp_logout_impl( + 49 => wire__crate__api__protocol__ProtocolProfile_openpgp_logout_impl( ptr, rust_vec_len, data_len, ), - 38 => wire__crate__api__protocol__ProtocolProfile_openpgp_pin_status_impl( + 50 => wire__crate__api__protocol__ProtocolProfile_openpgp_pin_status_impl( ptr, rust_vec_len, data_len, ), - 39 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_certificate_impl( + 51 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_certificate_impl( ptr, rust_vec_len, data_len, ), - 40 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_data_impl( + 52 => wire__crate__api__protocol__ProtocolProfile_openpgp_read_data_impl( ptr, rust_vec_len, data_len, ), - 41 => wire__crate__api__protocol__ProtocolProfile_openpgp_reset_retries_impl( + 53 => wire__crate__api__protocol__ProtocolProfile_openpgp_reset_retries_impl( ptr, rust_vec_len, data_len, ), - 42 => wire__crate__api__protocol__ProtocolProfile_openpgp_terminate_impl( + 54 => wire__crate__api__protocol__ProtocolProfile_openpgp_terminate_impl( ptr, rust_vec_len, data_len, ), - 43 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_admin_impl( + 55 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_admin_impl( ptr, rust_vec_len, data_len, ), - 44 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_code_impl( + 56 => wire__crate__api__protocol__ProtocolProfile_openpgp_unblock_with_code_impl( ptr, rust_vec_len, data_len, ), - 45 => wire__crate__api__protocol__ProtocolProfile_openpgp_verify_impl( + 57 => wire__crate__api__protocol__ProtocolProfile_openpgp_verify_impl( ptr, rust_vec_len, data_len, ), - 46 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_certificate_impl( + 58 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_certificate_impl( ptr, rust_vec_len, data_len, ), - 47 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_language_impl( + 59 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_language_impl( ptr, rust_vec_len, data_len, ), - 48 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_login_impl( + 60 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_login_impl( ptr, rust_vec_len, data_len, ), - 49 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_name_impl( + 61 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_name_impl( ptr, rust_vec_len, data_len, ), - 50 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_reset_code_impl( + 62 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_reset_code_impl( ptr, rust_vec_len, data_len, ), - 51 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_sex_impl( + 63 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_sex_impl( ptr, rust_vec_len, data_len, ), - 52 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_signature_pin_policy_impl( + 64 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_signature_pin_policy_impl( ptr, rust_vec_len, data_len, ), - 53 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_cache_time_impl( + 65 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_cache_time_impl( ptr, rust_vec_len, data_len, ), - 54 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_policy_impl( + 66 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_touch_policy_impl( ptr, rust_vec_len, data_len, ), - 55 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_url_impl( + 67 => wire__crate__api__protocol__ProtocolProfile_openpgp_write_url_impl( ptr, rust_vec_len, data_len, ), - 56 => wire__crate__api__protocol__ProtocolProfile_piv_algorithm_display_id_impl( + 68 => wire__crate__api__protocol__ProtocolProfile_piv_algorithm_display_id_impl( ptr, rust_vec_len, data_len, ), - 57 => { + 69 => { wire__crate__api__protocol__ProtocolProfile_piv_attest_impl(ptr, rust_vec_len, data_len) } - 58 => wire__crate__api__protocol__ProtocolProfile_piv_authenticate_management_impl( + 70 => wire__crate__api__protocol__ProtocolProfile_piv_authenticate_management_impl( ptr, rust_vec_len, data_len, ), - 59 => wire__crate__api__protocol__ProtocolProfile_piv_certificate_impl( + 71 => wire__crate__api__protocol__ProtocolProfile_piv_certificate_impl( ptr, rust_vec_len, data_len, ), - 60 => wire__crate__api__protocol__ProtocolProfile_piv_credential_impl( + 72 => wire__crate__api__protocol__ProtocolProfile_piv_credential_impl( ptr, rust_vec_len, data_len, ), - 61 => wire__crate__api__protocol__ProtocolProfile_piv_decapsulate_impl( + 73 => wire__crate__api__protocol__ProtocolProfile_piv_decapsulate_impl( ptr, rust_vec_len, data_len, ), - 62 => wire__crate__api__protocol__ProtocolProfile_piv_decrypt_impl( + 74 => wire__crate__api__protocol__ProtocolProfile_piv_decrypt_impl( ptr, rust_vec_len, data_len, ), - 63 => wire__crate__api__protocol__ProtocolProfile_piv_delete_certificate_impl( + 75 => wire__crate__api__protocol__ProtocolProfile_piv_delete_certificate_impl( ptr, rust_vec_len, data_len, ), - 64 => wire__crate__api__protocol__ProtocolProfile_piv_delete_key_impl( + 76 => wire__crate__api__protocol__ProtocolProfile_piv_delete_key_impl( ptr, rust_vec_len, data_len, ), - 65 => { + 77 => { wire__crate__api__protocol__ProtocolProfile_piv_derive_impl(ptr, rust_vec_len, data_len) } - 66 => wire__crate__api__protocol__ProtocolProfile_piv_generate_key_impl( + 78 => wire__crate__api__protocol__ProtocolProfile_piv_generate_key_impl( ptr, rust_vec_len, data_len, ), - 67 => wire__crate__api__protocol__ProtocolProfile_piv_import_ec_key_impl( + 79 => wire__crate__api__protocol__ProtocolProfile_piv_import_ec_key_impl( ptr, rust_vec_len, data_len, ), - 68 => wire__crate__api__protocol__ProtocolProfile_piv_import_ed25519_key_impl( + 80 => wire__crate__api__protocol__ProtocolProfile_piv_import_ed25519_key_impl( ptr, rust_vec_len, data_len, ), - 69 => wire__crate__api__protocol__ProtocolProfile_piv_import_pq_seed_impl( + 81 => wire__crate__api__protocol__ProtocolProfile_piv_import_pq_seed_impl( ptr, rust_vec_len, data_len, ), - 70 => wire__crate__api__protocol__ProtocolProfile_piv_import_rsa_key_impl( + 82 => wire__crate__api__protocol__ProtocolProfile_piv_import_rsa_key_impl( ptr, rust_vec_len, data_len, ), - 71 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_impl( + 83 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_impl( ptr, rust_vec_len, data_len, ), - 72 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_directory_impl( + 84 => wire__crate__api__protocol__ProtocolProfile_piv_metadata_directory_impl( ptr, rust_vec_len, data_len, ), - 73 => wire__crate__api__protocol__ProtocolProfile_piv_move_key_impl( + 85 => wire__crate__api__protocol__ProtocolProfile_piv_move_key_impl( ptr, rust_vec_len, data_len, ), - 74 => wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( + 86 => wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( ptr, rust_vec_len, data_len, ), - 75 => wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( + 87 => wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( ptr, rust_vec_len, data_len, ), - 76 => wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( + 88 => wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( ptr, rust_vec_len, data_len, ), - 77 => wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( + 89 => wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( ptr, rust_vec_len, data_len, ), - 78 => wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( + 90 => wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( ptr, rust_vec_len, data_len, ), - 79 => { + 91 => { wire__crate__api__protocol__ProtocolProfile_piv_sign_impl(ptr, rust_vec_len, data_len) } - 80 => wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( + 92 => wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( ptr, rust_vec_len, data_len, ), - 81 => wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( + 93 => wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( ptr, rust_vec_len, data_len, ), - 82 => wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( + 94 => wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( ptr, rust_vec_len, data_len, ), - 83 => wire__crate__api__protocol__ProtocolProfile_serial_impl(ptr, rust_vec_len, data_len), - 84 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), - 85 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), - 86 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( + 95 => wire__crate__api__protocol__ProtocolProfile_serial_impl(ptr, rust_vec_len, data_len), + 96 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), + 97 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), + 98 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( ptr, rust_vec_len, data_len, ), - 87 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), - 88 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( + 99 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), + 100 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 89 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), - 90 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), - 91 => wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len), - 92 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( + 101 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), + 102 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), + 103 => { + wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len) + } + 104 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( ptr, rust_vec_len, data_len, ), - 93 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), - 94 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), - 95 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), - 96 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( + 105 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), + 106 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), + 107 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), + 108 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( ptr, rust_vec_len, data_len, ), - 97 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), - 98 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( + 109 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), + 110 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( ptr, rust_vec_len, data_len, ), - 99 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( + 111 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 100 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), - 101 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), - 102 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), - 103 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), - 104 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), - 105 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), + 112 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), + 113 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), + 114 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), + 115 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), + 116 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), + 117 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } // Section: rust2dart +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for CtapPinSession { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for CtapPinToken { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -6409,6 +7121,8 @@ impl flutter_rust_bridge::IntoDart for crate::api::protocol::ProtocolStep { self.error.into_into_dart().into_dart(), self.profile.into_into_dart().into_dart(), self.admin.into_into_dart().into_dart(), + self.pin_session.into_into_dart().into_dart(), + self.pin_token.into_into_dart().into_dart(), ] .into_dart() } @@ -6488,6 +7202,20 @@ impl flutter_rust_bridge::IntoIntoDart } } +impl SseEncode for CtapPinSession { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for CtapPinToken { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for ProtocolOperation { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6502,6 +7230,28 @@ impl SseEncode for ProtocolProfile { } } +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for RustOpaqueMoi> { @@ -6711,6 +7461,26 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6895,6 +7665,8 @@ impl SseEncode for crate::api::protocol::ProtocolStep { >::sse_encode(self.error, serializer); >::sse_encode(self.profile, serializer); >::sse_encode(self.admin, serializer); + >::sse_encode(self.pin_session, serializer); + >::sse_encode(self.pin_token, serializer); } } @@ -6990,6 +7762,34 @@ mod io { flutter_rust_bridge::frb_generated_boilerplate_io!(); + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_canokey_console_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_canokey_console_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( ptr: *const std::ffi::c_void, @@ -7043,6 +7843,34 @@ mod web { flutter_rust_bridge::frb_generated_boilerplate_web!(); + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinSession( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCtapPinToken( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[wasm_bindgen] pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation( ptr: *const std::ffi::c_void, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9774c2b..cbb071f 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,14 +1,2 @@ pub mod api; mod frb_generated; - -// Retain fido2's upstream C ABI in the existing native library/framework. -#[cfg(not(target_arch = "wasm32"))] -#[used] -static FIDO2_ALLOC: extern "C" fn(usize) -> *mut u8 = fido2_crypto::fido2_alloc; -#[cfg(not(target_arch = "wasm32"))] -#[used] -static FIDO2_FREE: unsafe extern "C" fn(*mut u8, usize) = fido2_crypto::fido2_free; -#[cfg(not(target_arch = "wasm32"))] -#[used] -static FIDO2_CALL: unsafe extern "C" fn(*const u8, usize, *mut u8, usize) -> i32 = - fido2_crypto::fido2_call; diff --git a/test/controller/applets/webauthn/ctap_transmitter_test.dart b/test/controller/applets/webauthn/ctap_transmitter_test.dart deleted file mode 100644 index 5da26ba..0000000 --- a/test/controller/applets/webauthn/ctap_transmitter_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -@Tags(['native']) -library; - -import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/card_session.dart'; -import 'package:canokey_console/helper/utils/ctap_transmitter.dart'; -import 'package:canokey_console/helper/utils/protocol_operation.dart'; -import 'package:canokey_console/src/rust/frb_generated.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - setUpAll(() => RustLib.init()); - tearDownAll(RustLib.dispose); - - test('selects once, frames CTAP commands and follows GET RESPONSE', () async { - final transport = _QueueApduTransport(['9000', '006102', 'A19000']); - final transmitter = CtapTransmitter(transport: transport); - - final response = await transmitter.transceive([0x04]); - - expect(response.status, 0); - expect(response.data, [0xA1]); - expect(transport.commands, [ - '00A4040008A0000006472F0001', - '801000000104', - '80C0000002', - ]); - }); - - test('reuses the selection for subsequent messages', () async { - final transport = _QueueApduTransport(['9000', '009000', '2E9000']); - final transmitter = CtapTransmitter(transport: transport); - - await transmitter.transceive([0x04]); - final response = await transmitter.transceive([0x0A]); - - // A non-success CTAP status byte is data, not an error. - expect(response.status, 0x2E); - expect(response.data, isEmpty); - expect(transport.commands, [ - '00A4040008A0000006472F0001', - '801000000104', - '80100000010A', - ]); - }); - - test('explicit reselection selects again', () async { - final transport = _QueueApduTransport(['9000', '009000', '9000']); - final transmitter = CtapTransmitter(transport: transport); - - await transmitter.transceive([0x04]); - await transmitter.selectApplication(); - - expect(transport.commands, [ - '00A4040008A0000006472F0001', - '801000000104', - '00A4040008A0000006472F0001', - ]); - }); - - test('a new transmitter without lease evidence selects again', () async { - // Without a lease there is no shared selection evidence, so a new - // transmitter selects the applet before its first message. - final transport = _QueueApduTransport(['9000', '009000']); - final response = await CtapTransmitter( - transport: transport, - ).transceive([0x04]); - expect(response.status, 0); - expect(transport.commands, [ - '00A4040008A0000006472F0001', - '801000000104', - ]); - }); - - test('transmitters sharing a lease select the applet once', () async { - final sessions = CardSessions(); - final transport = _QueueApduTransport(['9000', '009000', '009000']); - await sessions.run((session) async { - session.bind(transport.transceive); - final first = CtapTransmitter( - transport: transport, - lease: session.lease, - ); - await first.transceive([0x04]); - // The selection belongs to the lease, so a second transmitter instance - // reuses it instead of selecting again. - final second = CtapTransmitter( - transport: transport, - lease: session.lease, - ); - final response = await second.transceive([0x0A]); - expect(response.status, 0); - }); - expect(transport.commands, [ - '00A4040008A0000006472F0001', - '801000000104', - '80100000010A', - ]); - }); - - test('an absent FIDO2 applet surfaces as UnsupportedDevice', () async { - final transport = _QueueApduTransport(['6A82']); - final transmitter = CtapTransmitter(transport: transport); - - await expectLater( - transmitter.transceive([0x04]), - throwsA( - isA().having( - (error) => error.details.kind, - 'kind', - 'UnsupportedDevice', - ), - ), - ); - expect(transport.commands, ['00A4040008A0000006472F0001']); - }); -} - -class _QueueApduTransport implements ApduTransport { - _QueueApduTransport(this.responses); - - final List responses; - final List commands = []; - int _responseIndex = 0; - - @override - Future transceive(String capdu) async { - commands.add(capdu); - return responses[_responseIndex++]; - } -} diff --git a/test/helper/utils/admin_card_test.dart b/test/helper/utils/admin_card_test.dart index 738c17a..512dab2 100644 --- a/test/helper/utils/admin_card_test.dart +++ b/test/helper/utils/admin_card_test.dart @@ -224,7 +224,7 @@ void main() { client.changePin('654321', currentPin: '123456'), _kind('AuthenticationFailed'), ); - expect(client.lastResponse, '63C2'); + expect(client.lastStatusWord, '63C2'); expect(client.lastProgress!.confirmedWrites, 0); expect(transport.commands, [_select, _verify]); await client.changePin('CanoKey密码', currentPin: '123456'); @@ -346,7 +346,7 @@ void main() { client.cancelPendingOperations(); finish.complete(); await rejected; - expect(client.lastResponse, isNull); + expect(client.lastStatusWord, isNull); expect(client.lastProgress!.confirmedWrites, 0); expect(client.lastProgress!.reprobeRequired, isTrue); expect(transport.commands, hasLength(4)); diff --git a/test/helper/utils/fido2_backend_test.dart b/test/helper/utils/fido2_backend_test.dart deleted file mode 100644 index ce5f08c..0000000 --- a/test/helper/utils/fido2_backend_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -@Tags(['native']) -library; - -import 'package:canokey_console/helper/utils/fido2_backend.dart'; -import 'package:cbor/cbor.dart'; -import 'package:convert/convert.dart'; -import 'package:fido2/fido2.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - setUpAll(initializeFido2Backend); - - test('initializes the packaged ABI and computes SHA-256', () async { - await initializeFido2Backend(); - expect( - hex.encode(RustCrypto.sha256([97, 98, 99])), - 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', - ); - }); - - for (final protocol in [PinProtocolV1(), PinProtocolV2()]) { - test( - 'credential enumeration and deletion use PIN v${protocol.version}', - () async { - final device = _CredentialDevice(); - final ctap = await Ctap2.create(device); - expect(ctap.info.algorithms!.single['type'], 'public-key'); - final manager = CredentialManagement( - ctap, - protocol, - List.filled(32, 7), - ); - final credentials = await manager.enumerateCredentialsMetadataOnly( - List.filled(32, 1), - ); - expect(credentials.single.user.name, 'alice'); - expect(credentials.single.coseAlgorithm, -49); - await manager.deleteCredential(credentials.single.credentialId); - expect(device.requests.map((r) => r[1]), [4, 6]); - for (final request in device.requests) { - expect(request[3], protocol.version); - expect(request[4], hasLength(protocol.version == 1 ? 16 : 32)); - } - }, - ); - test( - 'PIN v${protocol.version} encrypts, decrypts and uses the correct wire MAC', - () async { - final key = List.generate( - protocol.version == 1 ? 32 : 64, - (i) => i, - ); - final message = List.generate(32, (i) => 255 - i); - final encrypted = await protocol.encrypt(key, message); - expect(await protocol.decrypt(key, encrypted), message); - final mac = await protocol.authenticate(key, message); - expect(mac, hasLength(32)); - final wire = await protocol.authenticateParam(key, message); - expect(wire, protocol.version == 1 ? mac.sublist(0, 16) : mac); - }, - ); - } -} - -class _CredentialDevice extends CtapDevice { - final requests = >[]; - - @override - Future>> transceive(List command) async { - if (command.first == 4) { - return CtapResponse( - 0, - cbor.encode( - CborValue({ - 1: ['FIDO_2_1'], - 3: CborBytes(List.filled(16, 0)), - 4: {'credMgmt': true, 'clientPin': true}, - 6: [1, 2], - 10: [ - {'alg': -7, 'type': 'public-key'}, - ], - }), - ), - ); - } - expect(command.first, 0x0a); - final request = cbor.decode(command.sublist(1)).toObject() as Map; - requests.add(request); - if (request[1] == 6) return CtapResponse(0, []); - return CtapResponse( - 0, - cbor.encode( - CborValue({ - 6: { - 'id': CborBytes([1]), - 'name': 'alice', - }, - 7: { - 'id': CborBytes([2]), - 'type': 'public-key', - }, - 9: 1, - 10: 1, - 0x80: -49, - }), - ), - ); - } -} diff --git a/test/helper/utils/pass_card_test.dart b/test/helper/utils/pass_card_test.dart index 64642b3..63306d8 100644 --- a/test/helper/utils/pass_card_test.dart +++ b/test/helper/utils/pass_card_test.dart @@ -89,7 +89,7 @@ void main() { ), isTrue, ); - expect(client.lastResponse, '9000'); + expect(client.lastStatusWord, '9000'); expect(client.lastProgress!.confirmedWrites, 1); expect(client.lastProgress!.reprobeRequired, isTrue); expect(transport.commands, [_select, _verify, '00440100050202707701']); @@ -163,7 +163,7 @@ void main() { ), isFalse, ); - expect(client.lastResponse, '63C2'); + expect(client.lastStatusWord, '63C2'); expect( await client.setSlot( 1, @@ -174,7 +174,7 @@ void main() { ), isFalse, ); - expect(client.lastResponse, '6983'); + expect(client.lastStatusWord, '6983'); expect(transport.commands, [_select, _verify, _select, _verify]); }); }, @@ -189,7 +189,7 @@ void main() { client.setSlot(1, PassSlotType.static, 'pw', false, pin: '123456'), _kind('UnexpectedStatusWord'), ); - expect(client.lastResponse, '6A84'); + expect(client.lastStatusWord, '6A84'); expect(client.lastProgress!.confirmedWrites, 0); expect(client.lastProgress!.reprobeRequired, isTrue); expect(transport.commands, [_select, _verify, '00440100050202707700']); diff --git a/test/helper/utils/webauthn_card_test.dart b/test/helper/utils/webauthn_card_test.dart new file mode 100644 index 0000000..2be8cd1 --- /dev/null +++ b/test/helper/utils/webauthn_card_test.dart @@ -0,0 +1,369 @@ +@Tags(['native']) +library; + +import 'package:canokey_console/helper/utils/apdu_transport.dart'; +import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/helper/utils/webauthn_card.dart'; +import 'package:canokey_console/src/rust/frb_generated.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Golden fixtures are the upstream canokey-ctap known answers +// (crates/canokey-ctap/tests/client_pin.rs, credmgmt.rs), also pinned by the +// facade tests in rust/src/api/protocol.rs. The ephemeral scalar is generated +// by the facade CSPRNG, so ClientPIN commands are asserted by shape only; the +// fixture V1 token ciphertext decrypts to a garbage but usable token. +const _select = '00A4040008A0000006472F0001'; +const _peerKeyAgreement = + 'a101a5010203381820012158200d0918a04198474605615b6df90fdcb34791fb3ecb822f4b26eb6e4fc4511b9d22582019b90c1b83c0c35cfbbb31ead32bb52ae33622f57e3cc1638097ce97f430baba'; +const _tokenCiphertextV1 = + 'b98cc635132fa3ea8c191b7a4aa3e093ce926c35488221b4684fce766f3b14b0'; +const _rpBegin = + 'a303a26269646b6578616d706c652e636f6d646e616d65674578616d706c65045820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf0502'; +const _rpNext = + 'a203a1626964696f746865722e6f7267045820c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf'; +const _credentialMeta = + 'a406a26269644405060708646e616d6565616c69636507a2626964440102030464747970656a7075626c69632d6b6579090118803830'; + +/// getInfo fixture: versions [FIDO_2_0, FIDO_2_1], credMgmt true, clientPin +/// false, forcePinChange true, minPinLength 4, pinUvAuthProtocols [1, 2]. +const _getInfoPayload = 'a6' + '0182684649444f5f325f30684649444f5f325f31' + '0350244eb29ee0904e4981fe1f20f8d3b8f4' + '04a362726bf568637265644d676d74f569636c69656e7450696ef4' + '06820102' + '0cf5' + '0d04'; + +String _ctapOk(String payload) => '00${payload}9000'; + +void main() { + setUpAll(() => RustLib.init()); + tearDownAll(RustLib.dispose); + + test('getInfo parses tri-states, minPinLength and protocol versions', + () async { + final transport = _QueueApduTransport(['9000', _ctapOk(_getInfoPayload)]); + await _withClient(transport, (client) async { + final info = await client.getInfo(); + + expect(info.credMgmt, isTrue); + expect(info.clientPin, isFalse); + expect(info.forcePinChange, isTrue); + expect(info.minPinLength, 4); + expect(info.pinUvAuthProtocols, [1, 2]); + expect(info.pinProtocol, 2); + expect(transport.commands, [_select, '801000000104']); + }); + }); + + test('getInfo keeps unadvertised fields absent', () async { + final transport = _QueueApduTransport([ + '9000', + _ctapOk('a20181684649444f5f325f300350244eb29ee0904e4981fe1f20f8d3b8f4'), + ]); + await _withClient(transport, (client) async { + final info = await client.getInfo(); + + expect(info.credMgmt, isNull); + expect(info.clientPin, isNull); + expect(info.forcePinChange, isNull); + expect(info.minPinLength, isNull); + expect(info.pinUvAuthProtocols, isEmpty); + // No advertised pinUvAuthProtocols falls back to protocol V1. + expect(info.pinProtocol, 1); + }); + }); + + test('every operation selects the FIDO2 applet itself', () async { + final transport = _QueueApduTransport( + ['9000', _ctapOk(_getInfoPayload), '9000', _ctapOk(_getInfoPayload)]); + await _withClient(transport, (client) async { + await client.getInfo(); + await client.getInfo(); + + expect(transport.commands, [ + _select, + '801000000104', + _select, + '801000000104', + ]); + }); + }); + + test('an absent FIDO2 applet surfaces as UnsupportedDevice', () async { + final transport = _QueueApduTransport(['6A82']); + await _withClient(transport, (client) async { + await expectLater( + client.getInfo(), + throwsA(isA().having( + (error) => error.details.kind, + 'kind', + 'UnsupportedDevice', + )), + ); + expect(transport.commands, [_select]); + }); + }); + + test('a CTAP-level failure keeps the raw CTAP status byte', () async { + final transport = _QueueApduTransport(['9000', '309000']); + await _withClient(transport, (client) async { + await expectLater( + client.getInfo(), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'ConditionsNotSatisfied') + .having((e) => e.details.statusWord, 'statusWord', 0x30)), + ); + }); + }); + + test('a malformed getInfo response is a parsing failure', () async { + final transport = _QueueApduTransport([ + '9000', + _ctapOk('a10350244eb29ee0904e4981fe1f20f8d3b8f4'), + ]); + await _withClient(transport, (client) async { + await expectLater( + client.getInfo(), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidResponse') + .having((e) => e.details.phase, 'phase', 'Parsing')), + ); + }); + }); + + test('beginPinSession prefers V2 and drives the key agreement', () async { + final transport = + _QueueApduTransport(['9000', _ctapOk(_peerKeyAgreement)]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1, 2])); + + expect(session.protocolVersion, 2); + expect(transport.commands, [ + _select, + '801000000606A201020202', + ]); + session.close(); + }); + }); + + test('beginPinSession falls back to V1 and rejects short PINs before I/O', + () async { + final transport = + _QueueApduTransport(['9000', _ctapOk(_peerKeyAgreement)]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + + expect(session.protocolVersion, 1); + expect(transport.commands, [ + _select, + '801000000606A201010202', + ]); + + // PIN validation happens before any I/O. + await expectLater( + session.setPin('ab'), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'InvalidPin')), + ); + expect(transport.commands, hasLength(2)); + + // A closed session is inert. + session.close(); + session.close(); + await expectLater(session.setPin('1234'), throwsStateError); + expect(transport.commands, hasLength(2)); + }); + }); + + test('setPin and changePin complete over the upstream wire', () async { + // V1 setPin (zero IV by specification) then a V2 changePin; both carry + // non-deterministic ciphertext, so only success is asserted. + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // V1 session + '9000', '009000', // setPin + '9000', _ctapOk(_peerKeyAgreement), // V2 session + '9000', '009000', // changePin + ]); + await _withClient(transport, (client) async { + final v1 = await client.beginPinSession(_info(protocols: [1])); + await v1.setPin('1234'); + v1.close(); + + final v2 = await client.beginPinSession(_info(protocols: [1, 2])); + await v2.changePin('1234', '654321'); + v2.close(); + + expect(transport.commands[2], _select); + expect(transport.commands[3], startsWith('80100000')); + expect(transport.commands[3], contains('06A5')); + expect(transport.commands[6], _select); + expect(transport.commands[7], contains('06A6')); + }); + }); + + test('PIN failures keep the raw CTAP status byte', () async { + for (final (status, kind) in [(0x31, 'InvalidPin'), (0x32, 'PinBlocked')]) { + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // session + '9000', '${status.toRadixString(16).padLeft(2, '0').toUpperCase()}9000', + ]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + await expectLater( + session.getPinToken('1234'), + throwsA(isA() + .having((e) => e.details.kind, 'kind', kind) + .having((e) => e.details.statusWord, 'statusWord', status)), + ); + session.close(); + }); + } + }); + + test('token enumeration and deletion follow the golden transcript', + () async { + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // session + '9000', _ctapOk('a1025820$_tokenCiphertextV1'), // getPinToken + '9000', _ctapOk(_rpBegin), _ctapOk(_rpNext), // enumerateRps + '9000', _ctapOk(_credentialMeta), // enumerateCredentials + '9000', '009000', // deleteCredential + ]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + final token = await session.getPinToken('1234'); + session.close(); + expect(token.protocolVersion, 1); + + final rps = await token.enumerateRps(); + expect(rps, hasLength(2)); + expect(rps[0].id, 'example.com'); + expect(rps[0].name, 'Example'); + expect(rps[0].idHash, + List.generate(32, (index) => 0xA0 + index)); + expect(rps[1].id, 'other.org'); + expect(rps[1].name, isNull); + expect(rps[1].idHash, + List.generate(32, (index) => 0xC0 + index)); + + final credentials = await token.enumerateCredentials(rps[0].idHash); + expect(credentials, hasLength(1)); + final credential = credentials.single; + expect(credential.credentialId, [1, 2, 3, 4]); + expect(credential.userId, [5, 6, 7, 8]); + expect(credential.userName, 'alice'); + expect(credential.userDisplayName, isNull); + expect(credential.credProtect, isNull); + expect(credential.coseAlgorithm, -49); + expect(credential.publicKey, isNull); + + await token.deleteCredential(credential.credentialId); + token.close(); + + expect(transport.commands, [ + _select, + '801000000606A201010202', + _select, + allOf(startsWith('80100000'), contains('06A5')), + _select, + allOf(startsWith('80100000'), contains('0AA3')), + allOf(startsWith('80100000'), contains('0AA2010303')), + _select, + allOf(startsWith('80100000'), contains('0AA4'), contains('1880F5')), + _select, + allOf(startsWith('80100000'), contains('0AA4'), + contains('01020304')), + ]); + }); + }); + + test('enumeration without credentials yields an empty list', () async { + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // session + '9000', _ctapOk('a1025820$_tokenCiphertextV1'), // getPinToken + '9000', '2E9000', // enumerateRps Begin: NO_CREDENTIALS + ]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + final token = await session.getPinToken('1234'); + session.close(); + + expect(await token.enumerateRps(), isEmpty); + token.close(); + }); + }); + + test('deleting an unknown credential surfaces NotFound', () async { + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // session + '9000', _ctapOk('a1025820$_tokenCiphertextV1'), // getPinToken + '9000', '2E9000', // deleteCredential + ]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + final token = await session.getPinToken('1234'); + session.close(); + + await expectLater( + token.deleteCredential([1, 2, 3, 4]), + throwsA(isA() + .having((e) => e.details.kind, 'kind', 'NotFound') + .having((e) => e.details.statusWord, 'statusWord', 0x2E)), + ); + token.close(); + }); + }); + + test('a closed token rejects further operations without I/O', () async { + final transport = _QueueApduTransport([ + '9000', _ctapOk(_peerKeyAgreement), // session + '9000', _ctapOk('a1025820$_tokenCiphertextV1'), // getPinToken + ]); + await _withClient(transport, (client) async { + final session = await client.beginPinSession(_info(protocols: [1])); + final token = await session.getPinToken('1234'); + session.close(); + token.close(); + token.close(); + + await expectLater(token.enumerateRps(), throwsStateError); + expect(transport.commands, hasLength(4)); + }); + }); + + test('injected transports require an explicit session', () async { + final client = WebAuthnCardClient(transport: _QueueApduTransport([])); + await expectLater(client.getInfo(), throwsStateError); + }); +} + +WebAuthnInfo _info({required List protocols}) => WebAuthnInfo( + credMgmt: true, + clientPin: true, + forcePinChange: false, + minPinLength: 4, + pinUvAuthProtocols: protocols, + ); + +Future _withClient( + _QueueApduTransport transport, + Future Function(WebAuthnCardClient) action, +) { + final client = WebAuthnCardClient(transport: transport); + return client.withSession(() => action(client)); +} + +class _QueueApduTransport implements ApduTransport { + _QueueApduTransport(List responses) : responses = List.of(responses); + + final List responses; + final List commands = []; + int _responseIndex = 0; + + @override + Future transceive(String capdu) async { + commands.add(capdu); + return responses[_responseIndex++]; + } +} diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index 665535f..581d293 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -4,20 +4,18 @@ import 'package:canokey_console/src/rust/frb_generated.dart'; import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/apdu_transport.dart'; -import 'package:canokey_console/helper/utils/ctap_transmitter.dart'; -import 'package:canokey_console/helper/utils/fido2_backend.dart'; import 'package:canokey_console/helper/utils/ndef_card.dart'; import 'package:canokey_console/helper/utils/oath_card.dart'; import 'package:canokey_console/helper/utils/openpgp_card.dart'; import 'package:canokey_console/helper/utils/pass_card.dart'; import 'package:canokey_console/helper/utils/piv_card.dart'; import 'package:canokey_console/helper/utils/protocol_operation.dart'; +import 'package:canokey_console/helper/utils/webauthn_card.dart'; import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/models/oath.dart'; import 'package:canokey_console/models/openpgp.dart'; import 'package:canokey_console/models/pass.dart'; import 'package:ccid/ccid.dart'; -import 'package:fido2/fido2.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -57,7 +55,7 @@ class ConsoleSmoke { _oathClient = OathCardClient(transport: _clientTransport('oath')); _passClient = PassCardClient(transport: _clientTransport('pass')); _pivClient = PivCardClient(transport: _clientTransport('piv')); - _webAuthnTransmitter = CtapTransmitter( + _webAuthnClient = WebAuthnCardClient( transport: _clientTransport('webauthn'), ); } @@ -71,7 +69,7 @@ class ConsoleSmoke { late final OathCardClient _oathClient; late final PassCardClient _passClient; late final PivCardClient _pivClient; - late final CtapTransmitter _webAuthnTransmitter; + late final WebAuthnCardClient _webAuthnClient; late FunctionSetVersion _functionSet; late String _adminSerial; late bool _initialNdefReadonly; @@ -929,34 +927,20 @@ class ConsoleSmoke { 'WebAuthn getInfo returned CTAP status ${bytes.first}', ); _expect(bytes.length > 1, 'WebAuthn getInfo returned no CBOR payload'); - final info = AuthenticatorInfo.decode(bytes.sublist(1)); - _expect(info.versions.isNotEmpty, 'WebAuthn reports no protocol versions'); - _expect( - info.versions.contains('FIDO_2_0'), - 'WebAuthn does not advertise FIDO_2_0', - ); - _expect(info.aaguid.length == 16, 'WebAuthn AAGUID must be 16 bytes'); - _expect( - info.maxMsgSize == null || info.maxMsgSize! > 0, - 'WebAuthn max message size is invalid', - ); - _expect( - info.algorithms == null || info.algorithms!.isNotEmpty, - 'WebAuthn algorithms list is empty', - ); - final clientResponse = await _webAuthnTransmitter.transceive([0x04]); - _expect( - clientResponse.status == 0, - 'Console WebAuthn transmitter returned CTAP status ' - '${clientResponse.status}', - ); - final clientInfo = AuthenticatorInfo.decode(clientResponse.data); - _expect( - clientInfo.versions.contains('FIDO_2_0'), - 'Console WebAuthn transmitter did not parse FIDO_2_0', - ); - stdout.writeln('ok: webauthn.client.get_info'); + await _webAuthnClient.withSession(() async { + final info = await _webAuthnClient.getInfo(); + _expect( + info.clientPin != null, + 'WebAuthn client getInfo did not advertise clientPin', + ); + _expect( + info.pinUvAuthProtocols.contains(1) || + info.pinUvAuthProtocols.contains(2), + 'WebAuthn client getInfo advertised no PIN protocol', + ); + stdout.writeln('ok: webauthn.client.get_info'); + }); } Future _passApplet() async { @@ -1089,7 +1073,6 @@ void main() { Future _runSmoke() async { await RustLib.init(); addTearDown(RustLib.dispose); - await initializeFido2Backend(); final environment = Platform.environment; _expect(environment['CANOKEY_USBIP'] == '1', 'CANOKEY_USBIP must be 1'); final expectedVersion = _requiredEnvironment('CANOKEY_FIRMWARE_VERSION'); diff --git a/test/views/applets/webauthn/webauthn_page_test.dart b/test/views/applets/webauthn/webauthn_page_test.dart index f0c06e6..b9655ef 100644 --- a/test/views/applets/webauthn/webauthn_page_test.dart +++ b/test/views/applets/webauthn/webauthn_page_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:canokey_console/models/canokey.dart'; import 'package:canokey_console/views/applets/webauthn/dialogs/sm2_config_dialog.dart'; import 'package:canokey_console/controller/applets/webauthn/webauthn_controller.dart'; @@ -10,7 +12,6 @@ import 'package:canokey_console/views/applets/webauthn/dialogs/view_user_id_dial import 'package:canokey_console/views/applets/webauthn/webauthn_page.dart'; import 'package:canokey_console/views/applets/webauthn/widgets/webauthn_credential_grid.dart'; import 'package:canokey_console/views/applets/webauthn/widgets/webauthn_item_card.dart'; -import 'package:fido2/fido2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -29,7 +30,7 @@ class _Controller extends WebAuthnController { @override void onReady() {} @override - Future delete(PublicKeyCredentialDescriptor credentialId) async { + Future delete(List credentialId) async { deletes++; } } @@ -39,7 +40,7 @@ WebAuthnItem _item(int id, {bool long = false}) => WebAuthnItem( userName: long ? 'long-account-name' * 8 : 'account$id', userDisplayName: long ? 'Long display name ' * 8 : 'Person $id', userId: [65, 66, id], - credentialId: PublicKeyCredentialDescriptor(type: 'public-key', id: [id]), + credentialId: Uint8List.fromList([id]), ); void main() { diff --git a/web/index.html b/web/index.html index d108d98..02ca048 100644 --- a/web/index.html +++ b/web/index.html @@ -22,7 +22,6 @@ -
From 69ef2b1b4eb5b959b3558d4fff4469799c3f7b1e Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 19:26:31 +0800 Subject: [PATCH 06/19] refactor: adopt upstream firmware authentication-gate modeling Bump libcanokey to 9110bc71. The PIV algorithm-extension read now goes through the profile-based upstream operation: Access::Management authenticates and reads in one operation on 3.0.x, and the capability gate rejects unsupported or unauthenticated reads before any I/O, so the version-hardcoded fallback and the manual select/prepare/authenticate choreography are gone. CTAP status bytes now arrive via Error::application_status. Pass slot reads rely on the upstream protected-read gate. --- docs/libcanokey-migration.md | 81 ++++++-- .../applets/piv/piv_controller.dart | 15 +- lib/helper/utils/pass_card.dart | 8 +- lib/helper/utils/piv_card.dart | 52 +++-- lib/src/rust/api/protocol.dart | 19 +- lib/src/rust/frb_generated.dart | 132 +++++++++---- rust/Cargo.lock | 20 +- rust/Cargo.toml | 4 +- rust/src/api/protocol.rs | 187 ++++++++++++++++-- rust/src/frb_generated.rs | 134 +++++++++---- test/helper/utils/pass_card_test.dart | 33 +++- test/helper/utils/piv_card_test.dart | 71 +++---- test/usbip/console_smoke.dart | 11 +- 13 files changed, 577 insertions(+), 190 deletions(-) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index baa6739..cff59cc 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -2,7 +2,8 @@ The first increment uses the Rust facade through Flutter Rust Bridge, following [canokey-pkcs11#5](https://github.com/canokeys/canokey-pkcs11/pull/5). Cargo pins -libcanokey to the same `95e1930ea86eb248d0295bce9880cee5c65fd85e` revision. +libcanokey to the `9110bc71a0b00f3517b96651ed63e030f8fffadc` revision (with the +facade's opt-in `clientpin` feature). The C ABI is unnecessary in Console's existing Rust library. ## Implemented @@ -70,11 +71,16 @@ The C ABI is unnecessary in Console's existing Rust library. - Errors retain category, phase, status word, credential reference and retry count as separate fields. Transport errors are propagated without conversion. - Only an unsupported algorithm-configuration instruction permits the existing - firmware defaults, plus one firmware-scoped exception: 3.0.x gates that read - behind management-key authentication (every SELECT resets the status), so an - unauthenticated capabilities read takes the defaults fallback exactly on - 3.0.x, and `readAlgorithmExtensions` accepts an optional management key to - authenticate within the same selection. Other security failures, malformed + firmware defaults, plus the management-gated read on 3.0.x: the upstream + `Capability::PivProtectedAlgorithmConfigRead` models that gate, so the + profile-based `pivReadAlgorithmConfig` binding rejects an unauthenticated + read with SecurityStatusNotSatisfied there (the card answers 6982), and the + unauthenticated capabilities read takes the defaults fallback on exactly + that kind, without a version check. `readAlgorithmExtensions` still accepts + an optional management key; upstream then SELECTs, authenticates and reads + within the same operation, and the capability check runs at construction + before any authentication, so firmware without the read never sees a + GENERAL AUTHENTICATE. Other security failures, malformed responses and communication errors never silently choose default algorithms. - OpenPGP card-info reads, PW1/PW3 credential operations and all @@ -126,8 +132,10 @@ The C ABI is unnecessary in Console's existing Rust library. `deleteCredential`. PinSession and PinToken are opaque FRB handles holding zeroizing Rust state; ephemeral scalars and V2 IVs are generated in the facade from the CSPRNG (`rand`), never supplied by Dart. For CTAP-level - failures `ProtocolError.statusWord` carries the raw CTAP status byte widened - to u16, not an ISO 7816 status word. Enumeration results use self-delimiting + failures upstream carries the raw CTAP status byte in + `Error::application_status` (`status_word` is ISO-only again); the bridge + keeps the Dart contract by widening it into `ProtocolError.statusWord`, + which therefore is not an ISO 7816 status word for CTAP failures. Enumeration results use self-delimiting byte encodings; Dart never parses CBOR. The Dart `WebAuthnCardClient` (`lib/helper/utils/webauthn_card.dart`) decodes those encodings, prefers pinUvAuthProtocol V2 when advertised, and owns session/token handle @@ -138,17 +146,23 @@ The C ABI is unnecessary in Console's existing Rust library. getInfo. The Dart fido2 package, the `fido2_crypto` crate (and its C ABI in `lib.rs`), the `web/fido2` WASM artifacts and the raw CTAP transmitter layer were removed; no Dart CBOR/ClientPin framing remains. -- Pass reads/writes use `adminPassSlots` (read, not PIN-protected) and - `adminSetPassSlot` (protected) with the same thin client shape as +- Pass reads/writes use `adminPassSlots` and + `adminSetPassSlot` with the same thin client shape as admin_card; both reuse the lease's verified Admin session when one is - recorded and otherwise SELECT + verify an explicit per-request PIN. Unknown - slot types display as unknown, and a write reporting `reprobeRequired` + recorded and otherwise SELECT + verify an explicit per-request PIN. INS 43 + sits behind the firmware Admin-PIN gate on every audited firmware, so + upstream rejects a bare read (no recorded session, no PIN) at construction + with SecurityStatusNotSatisfied, before any I/O. Unknown slot types display + as unknown, and a write reporting `reprobeRequired` invalidates the profile. The manual 0043/0044 commands were removed. - New facade bindings in `rust/src/api/protocol.rs`: `adminPassSlots`/ `adminSetPassSlot` (including the `AdminValueKind.passSlots` encoding), `Access::Existing` support (the `existing` parameter) on the whole Admin request path (`adminRead`/`adminConfigure`/`adminAction` and the PASS slot - operations), `pivImportPqSeed`, `oathSetDefault`, `ndefReadCapability`/ + operations), `pivImportPqSeed`, `pivReadAlgorithmConfig` (profile-based, + optional management key; replaces the profile-free + `PivReadOperation.algorithmConfiguration`), `oathSetDefault`, + `ndefReadCapability`/ `ndefReadMessage`/`ndefWriteMessage` and `ctapSelectApplication`/ `ctapTransceiveSelected`/`ctapTransceive`. The CTAP2 client-layer increment adds `ctapGetInfo`, `ctapBeginPinSession`, the `CtapPinSession` methods @@ -577,3 +591,44 @@ section), `flutter test --no-pub --tags native` passed (130 tests), and fido2 reference left in the web output. The FRB WASM package was not regenerated (no Rust facade change); no physical card, browser transport or USB/IP firmware matrix was exercised. + +## Firmware authentication-gate modeling (2026-09-16, rev 9110bc7) + +The pin moved to `9110bc71a0b00f3517b96651ed63e030f8fffadc`, which models the +firmware authentication gates on reads upstream: Admin INS 43/44 (PASS) reads +are unconditionally protected, so `Access::None` fails at construction with +SecurityStatusNotSatisfied and zero I/O; the new +`Capability::PivProtectedAlgorithmConfigRead` (3.0.0–3.0.3) makes +`piv::read_algorithm_config` reject `Access::None`/`Access::Pin` at +construction while allowing `Existing`/`Management`. The facade gained the +profile-based `pivReadAlgorithmConfig` binding: without a key it uses +`Access::Existing`; with a key it uses +`Access::Management(ManagementAuthentication::external(...))`, and upstream +SELECTs, authenticates (GENERAL AUTHENTICATE) and reads within one operation. +The capability check runs at construction before any authentication, so +firmware without the INS EE read never receives a GENERAL AUTHENTICATE. The +Dart client's manual select→prepare→authenticate→read orchestration was +removed; `PivReadOperation.algorithmConfiguration` (profile-free +`read_configuration_selected`) was removed from the bridge, and the +controller's `>= 3.0.0 && < 3.1.0` fallback gate was replaced by an +unconditional SecurityStatusNotSatisfied fallback. CTAP failures now arrive in +upstream `Error::application_status` (status_word is ISO-only again); the +bridge widens the byte into `ProtocolError.statusWord`, so the Dart contract +is unchanged. The facade's `canokey` dependency enables the now-opt-in +`clientpin` feature. The OATH `Outcome::Serial`/`ChallengeResponse` variants +(the renamed vendor challenge-response API) are not exposed by the facade and +map to InvalidResponse if ever produced. + +Validation: `cargo test --locked` passed in full (78 tests, including new +facade coverage for the application_status mapping, the capability-gate +precedes-authentication ordering on 2.0.0, the 3.0.3 SELECT+GA+read transcript +against the known TDES answer, the unauthenticated 6982 path and the PassSlots +bare-read construction gate). FRB 2.13.0 regeneration, `cargo build --release +--locked`, `cargo check --target wasm32-unknown-unknown --locked`, +all-targets clippy and rustfmt passed. `flutter test --no-pub` passed (388 +tests), `flutter test --no-pub --tags native` passed (131 tests, including the +rewritten Pass transcripts: bare read fails at construction with zero I/O, +PIN reads keep their SELECT+VERIFY+read transcripts), and `dart analyze` +reports no errors. The usbip smoke passes the default management key +unconditionally for the extension read. No physical card, browser transport +or USB/IP firmware matrix was exercised for this increment. diff --git a/lib/controller/applets/piv/piv_controller.dart b/lib/controller/applets/piv/piv_controller.dart index efd3126..e0600d3 100644 --- a/lib/controller/applets/piv/piv_controller.dart +++ b/lib/controller/applets/piv/piv_controller.dart @@ -128,8 +128,8 @@ class PivController extends PollingController { } disabledMessage = null; - await _refreshCapabilities(); await _client.prepare(); + await _refreshCapabilities(); slots.clear(); certificateSlots.clear(); certificateBytes.clear(); @@ -163,15 +163,12 @@ class PivController extends PollingController { try { config = await _client.readAlgorithmExtensions(); } on ProtocolException catch (error) { - // Firmware 3.0.x gates this read behind management-key authentication - // (6982) and Console reads capabilities before any authentication, so - // keep the firmware-defaults fallback for exactly that known gate; + // Console reads capabilities before any authentication; on 3.0.x the + // read sits behind management-key authentication (upstream capability + // PivProtectedAlgorithmConfigRead), so the card rejects it with 6982. + // That security rejection alone permits the firmware-defaults fallback; // every other failure remains visible. - final gatedByManagementKey = - error.details.kind == 'SecurityStatusNotSatisfied' && - firmwareVersion.compareTo(const FirmwareVersion(3, 0, 0)) >= 0 && - firmwareVersion.compareTo(const FirmwareVersion(3, 1, 0)) < 0; - if (!gatedByManagementKey) rethrow; + if (error.details.kind != 'SecurityStatusNotSatisfied') rethrow; } if (config != null) { algorithmExtensionConfig = config; diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index 4924331..34e7902 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -17,9 +17,11 @@ class PassCardClient extends AdminSessionCardClient { /// a slot write, including an uncertain write; never auto-reprobe. Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); - /// Reads both slots. Firmware gates INS 43 behind Admin authentication, so - /// callers pass the lease's verified PIN; a still-valid recorded Admin - /// authentication takes precedence and the PIN is not sent at all. + /// Reads both slots. INS 43 sits behind the firmware Admin-PIN gate on + /// every audited firmware: a still-valid recorded Admin authentication is + /// reused (`Access::Existing`), otherwise callers pass the lease's verified + /// PIN. A bare read without either is rejected at construction with + /// SecurityStatusNotSatisfied, before any I/O. Future> readSlots({String? pin}) async { final result = await executeAdminRequest( (profile, pinBytes, existing) => diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index 34bf065..e8214ae 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -409,32 +409,54 @@ class PivCardClient extends ProfileCardClient { } } - /// Reads the algorithm extension configuration. Firmware 3.0.x gates this - /// read behind management-key authentication and every SELECT resets that - /// status, so callers on such firmware pass [managementKey]: the client - /// SELECTs, prepares and authenticates in the same selection before reading. + /// Explicit discovery only when the prepared binding is missing or stale. + /// A still-valid binding is reused as-is; discovery is never repeated + /// implicitly. + Future prepareIfStale() async { + final binding = currentProfile; + if (binding != null && identical(binding.lease, lease)) { + try { + binding.check(); + cancellation.check(); + return; + } on StateError { + // Fall through to explicit discovery below. + } + } + await prepare(); + } + + /// Reads the algorithm extension configuration through the upstream + /// profile-based operation. On 3.0.x firmware the read sits behind + /// management-key authentication (upstream capability + /// PivProtectedAlgorithmConfigRead), so callers on such firmware pass + /// [managementKey]: upstream SELECTs, authenticates and reads within one + /// operation. Without a key the caller's selected transaction is reused. + /// Firmware without the read fails the capability check at construction, + /// before any authentication I/O. Future readAlgorithmExtensions({ String? managementKey, AlgorithmType managementKeyAlgorithm = AlgorithmType.tdes, }) async { - await select(); - if (managementKey != null) { - await prepare(); - if (!await authenticateManagementKey( - managementKey, - managementKeyAlgorithm, - )) { - throw StateError('PIV management key authentication failed'); - } - } + await prepareIfStale(); + final keyBytes = managementKey == null + ? null + : Uint8List.fromList(hex.decode(managementKey)); try { - final data = await _read(PivReadOperation.algorithmConfiguration); + final data = await _executePrepared( + (profile) => profile.pivReadAlgorithmConfig( + managementKey: keyBytes, + managementKeyAlgorithm: managementKeyAlgorithm.value, + ), + ); return PivAlgorithmExtensionConfig.decode(data); } on ProtocolException catch (error) { // Only an unavailable instruction permits the existing firmware fallback. // Security, malformed-data and unexpected card failures remain errors. if (error.details.kind == 'UnsupportedFeature') return null; rethrow; + } finally { + keyBytes?.fillRange(0, keyBytes.length, 0); } } diff --git a/lib/src/rust/api/protocol.dart b/lib/src/rust/api/protocol.dart index 4f64888..ee0a36a 100644 --- a/lib/src/rust/api/protocol.dart +++ b/lib/src/rust/api/protocol.dart @@ -502,6 +502,19 @@ abstract class ProtocolProfile implements RustOpaqueInterface { ProtocolOperation pivMoveKey({required int source, required int target}); + /// Read the PIV algorithm extension configuration (INS EE). Without a + /// management key this reuses the caller's selected transaction + /// (`Access::Existing`); with one, upstream SELECTs, authenticates + /// (external GENERAL AUTHENTICATE) and reads within the same operation. + /// On 3.0.x firmware (`PivProtectedAlgorithmConfigRead`) an + /// unauthenticated read is rejected with SecurityStatusNotSatisfied; on + /// firmware without the read the capability check fails at construction, + /// before any authentication I/O. + ProtocolOperation pivReadAlgorithmConfig({ + Uint8List? managementKey, + required int managementKeyAlgorithm, + }); + /// Return the normalized object value. libcanokey owns framing and parsing. ProtocolOperation pivReadObject({required List objectId}); @@ -714,12 +727,14 @@ enum PivCredentialOperation { } /// Initial PIV operations; reads require an already selected PIV application. -enum PivReadOperation { select, version, algorithmConfiguration, pinStatus } +enum PivReadOperation { select, version, pinStatus } /// Structured, payload-free protocol failure. Transport errors remain in Dart. /// For CTAP-level failures `status_word` carries the raw CTAP status byte /// widened to u16 (for example 0x0031 for PIN_INVALID), NOT an ISO 7816 -/// status word; see canokey-ctap's status table. +/// status word; see canokey-ctap's status table. Upstream keeps that byte in +/// `Error::application_status` (`status_word` is ISO-only); the bridge maps it +/// here so the Dart contract is unchanged. class ProtocolError { final String kind; final String phase; diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index e057924..824eca5 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.13.0'; @override - int get rustContentHash => 1993561274; + int get rustContentHash => -729938; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -561,6 +561,12 @@ abstract class RustLibApi extends BaseApi { required int target, }); + ProtocolOperation crateApiProtocolProtocolProfilePivReadAlgorithmConfig({ + required ProtocolProfile that, + Uint8List? managementKey, + required int managementKeyAlgorithm, + }); + ProtocolOperation crateApiProtocolProtocolProfilePivReadObject({ required ProtocolProfile that, required List objectId, @@ -3880,6 +3886,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that", "source", "target"], ); + @override + ProtocolOperation crateApiProtocolProtocolProfilePivReadAlgorithmConfig({ + required ProtocolProfile that, + Uint8List? managementKey, + required int managementKeyAlgorithm, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolProfile( + that, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(managementKey, serializer); + sse_encode_u_8(managementKeyAlgorithm, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerProtocolOperation, + decodeErrorData: null, + ), + constMeta: + kCrateApiProtocolProtocolProfilePivReadAlgorithmConfigConstMeta, + argValues: [that, managementKey, managementKeyAlgorithm], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiProtocolProtocolProfilePivReadAlgorithmConfigConstMeta => + const TaskConstMeta( + debugName: "ProtocolProfile_piv_read_algorithm_config", + argNames: ["that", "managementKey", "managementKeyAlgorithm"], + ); + @override ProtocolOperation crateApiProtocolProtocolProfilePivReadObject({ required ProtocolProfile that, @@ -3894,7 +3938,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(objectId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; }, codec: SseCodec( decodeSuccessData: @@ -3930,7 +3974,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(pinRetries, serializer); sse_encode_u_8(pukRetries, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; }, codec: SseCodec( decodeSuccessData: @@ -3966,7 +4010,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_list_prim_u_8_loose(raw, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; }, codec: SseCodec( decodeSuccessData: @@ -4004,7 +4048,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_u_8(slot, serializer); sse_encode_String(name, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 90)!; }, codec: SseCodec( decodeSuccessData: @@ -4045,7 +4089,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_u_8(touch, serializer); sse_encode_bool(updateProtected, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 90)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; }, codec: SseCodec( decodeSuccessData: @@ -4086,7 +4130,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(input, serializer); sse_encode_u_8(inputKind, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 92)!; }, codec: SseCodec( decodeSuccessData: @@ -4126,7 +4170,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(mode, serializer); sse_encode_list_prim_u_8_loose(message, serializer); sse_encode_opt_list_prim_u_8_strict(userId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 92)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; }, codec: SseCodec( decodeSuccessData: @@ -4172,7 +4216,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(userId, serializer); sse_encode_opt_list_prim_u_8_strict(peerId, serializer); sse_encode_u_16(keyLen, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; }, codec: SseCodec( decodeSuccessData: @@ -4226,7 +4270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); sse_encode_list_prim_u_8_loose(objectId, serializer); sse_encode_list_prim_u_8_loose(data, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; }, codec: SseCodec( decodeSuccessData: @@ -4258,7 +4302,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, @@ -4290,7 +4334,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(cardData, serializer); sse_encode_bool(generatedResponse, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; }, codec: SseCodec( decodeSuccessData: sse_decode_piv_public_key_data, @@ -4316,7 +4360,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(pngFile, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -4348,7 +4392,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(key, serializer); sse_encode_list_prim_u_8_loose(challenge, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 99)!; }, codec: SseCodec( decodeSuccessData: sse_decode_list_prim_u_8_strict, @@ -4380,7 +4424,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_loose(certificationRequestInfo, serializer); sse_encode_u_8(algorithm, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 99)!; + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 100, + )!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -4415,7 +4463,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 101, )!; }, codec: SseCodec( @@ -4449,7 +4497,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 102, )!; }, codec: SseCodec( @@ -4475,7 +4523,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 103, )!; }, codec: SseCodec( @@ -4504,7 +4552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 104, )!; }, codec: SseCodec( @@ -4538,7 +4586,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 105, )!; }, codec: SseCodec( @@ -4568,7 +4616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 106, )!; }, codec: SseCodec( @@ -4598,7 +4646,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 107, )!; }, codec: SseCodec( @@ -4636,7 +4684,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 108, )!; }, codec: SseCodec( @@ -4674,7 +4722,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 109, )!; }, codec: SseCodec( @@ -4716,7 +4764,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 110, )!; }, codec: SseCodec( @@ -4766,7 +4814,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 111, )!; }, codec: SseCodec( @@ -4801,7 +4849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 112, )!; }, codec: SseCodec( @@ -4831,7 +4879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 113, )!; }, codec: SseCodec( @@ -4858,7 +4906,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 114, )!; }, codec: SseCodec( @@ -4885,7 +4933,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 115, )!; }, codec: SseCodec( @@ -4916,7 +4964,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 116, )!; }, codec: SseCodec( @@ -4950,7 +4998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 117, )!; }, codec: SseCodec( @@ -4987,7 +5035,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 118, )!; }, codec: SseCodec( @@ -7988,6 +8036,24 @@ class ProtocolProfileImpl extends RustOpaque implements ProtocolProfile { target: target, ); + /// Read the PIV algorithm extension configuration (INS EE). Without a + /// management key this reuses the caller's selected transaction + /// (`Access::Existing`); with one, upstream SELECTs, authenticates + /// (external GENERAL AUTHENTICATE) and reads within the same operation. + /// On 3.0.x firmware (`PivProtectedAlgorithmConfigRead`) an + /// unauthenticated read is rejected with SecurityStatusNotSatisfied; on + /// firmware without the read the capability check fails at construction, + /// before any authentication I/O. + ProtocolOperation pivReadAlgorithmConfig({ + Uint8List? managementKey, + required int managementKeyAlgorithm, + }) => RustLib.instance.api + .crateApiProtocolProtocolProfilePivReadAlgorithmConfig( + that: this, + managementKey: managementKey, + managementKeyAlgorithm: managementKeyAlgorithm, + ); + /// Return the normalized object value. libcanokey owns framing and parsing. ProtocolOperation pivReadObject({required List objectId}) => RustLib.instance.api.crateApiProtocolProtocolProfilePivReadObject( diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1008f66..3179e43 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,7 +355,7 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-admin", "canokey-compat", @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-compat", "canokey-protocol", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-protocol", ] @@ -387,7 +387,7 @@ dependencies = [ [[package]] name = "canokey-ctap" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "aes 0.8.4", "canokey-protocol", @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-compat", "canokey-protocol", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "canokey-ndef" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-protocol", ] @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-compat", "canokey-protocol", @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "canokey-compat", "canokey-key", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=f043d5390ee2f6810fa543f8e1e9f9acac591e7d#f043d5390ee2f6810fa543f8e1e9f9acac591e7d" +source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" dependencies = [ "thiserror 2.0.20", "zeroize", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ce044c7..119ea67 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "f043d5390ee2f6810fa543f8e1e9f9acac591e7d" } -canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "f043d5390ee2f6810fa543f8e1e9f9acac591e7d" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "9110bc71a0b00f3517b96651ed63e030f8fffadc", features = ["clientpin"] } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "9110bc71a0b00f3517b96651ed63e030f8fffadc" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs index 5995e77..4034c36 100644 --- a/rust/src/api/protocol.rs +++ b/rust/src/api/protocol.rs @@ -12,7 +12,6 @@ use canokey_protocol::operation::{conversation, ResponseData}; pub enum PivReadOperation { Select, Version, - AlgorithmConfiguration, PinStatus, } @@ -192,7 +191,9 @@ fn pass_slots_data(slots: admin::PassSlots) -> Vec { /// Structured, payload-free protocol failure. Transport errors remain in Dart. /// For CTAP-level failures `status_word` carries the raw CTAP status byte /// widened to u16 (for example 0x0031 for PIN_INVALID), NOT an ISO 7816 -/// status word; see canokey-ctap's status table. +/// status word; see canokey-ctap's status table. Upstream keeps that byte in +/// `Error::application_status` (`status_word` is ISO-only); the bridge maps it +/// here so the Dart contract is unchanged. pub struct ProtocolError { pub kind: String, pub phase: String, @@ -206,7 +207,10 @@ impl From for ProtocolError { Self { kind: format!("{:?}", error.kind), phase: format!("{:?}", error.phase), - status_word: error.status_word.map(|sw| sw.raw()), + status_word: match error.application_status { + Some(status) => Some(u16::from(status)), + None => error.status_word.map(|sw| sw.raw()), + }, reference: error.reference.map(|reference| format!("{reference:?}")), retries_remaining: error.retries_remaining, } @@ -558,6 +562,43 @@ impl ProtocolProfile { })(); ProtocolOperation::from_operation(operation) } + + /// Read the PIV algorithm extension configuration (INS EE). Without a + /// management key this reuses the caller's selected transaction + /// (`Access::Existing`); with one, upstream SELECTs, authenticates + /// (external GENERAL AUTHENTICATE) and reads within the same operation. + /// On 3.0.x firmware (`PivProtectedAlgorithmConfigRead`) an + /// unauthenticated read is rejected with SecurityStatusNotSatisfied; on + /// firmware without the read the capability check fails at construction, + /// before any authentication I/O. + #[flutter_rust_bridge::frb(sync)] + pub fn piv_read_algorithm_config( + &self, + management_key: Option>, + management_key_algorithm: u8, + ) -> ProtocolOperation { + let management_key = management_key.map(SecretBytes::new); + ProtocolOperation::from_operation((|| { + let profile = self + .inner + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::OperationStateError))?; + let access = match management_key { + None => piv::Access::Existing, + Some(key) => { + let algorithm = match management_key_algorithm { + 0x03 => piv::ManagementKeyAlgorithm::Tdes, + 0x0a => piv::ManagementKeyAlgorithm::Aes192, + _ => return Err(Error::new(ErrorKind::UnsupportedAlgorithm)), + }; + let key = piv::ManagementKey::from_bytes(algorithm, key.as_bytes())?; + piv::Access::Management(piv::ManagementAuthentication::external(key)) + } + }; + piv::read_algorithm_config(profile, access, OperationOptions::default()) + .map(Inner::Configuration) + })()) + } /// Read metadata in the caller's already selected PIV session. No probing, /// SELECT or authentication is inserted, including after VERIFY. #[flutter_rust_bridge::frb(sync)] @@ -2262,7 +2303,8 @@ fn ctap_rps_data(entries: Vec) -> Vec { /// (i64 BE, CanoKey extension key 0x80), or 0x02 publicKey with the resolved /// COSE algorithm (i64 BE, i64::MIN when unresolvable) followed by /// `key_len (u16 BE) | canonical CBOR COSE key` (opaque pass-through). -fn ctap_credentials_data(entries: Vec) -> Vec { +/// COSE key re-encoding fails without any I/O and stays a protocol failure. +fn ctap_credentials_data(entries: Vec) -> Result, Error> { let mut out = Vec::new(); for entry in entries { let id = &entry.credential_id.id; @@ -2298,7 +2340,7 @@ fn ctap_credentials_data(entries: Vec) -> Vec) -> Vec unreachable!("credential entries carry exactly one key form"), } } - out + Ok(out) } /// Fresh V2 IV (16 CSPRNG bytes); protocol V1 uses the spec-mandated zero @@ -2619,9 +2661,6 @@ impl ProtocolOperation { } PivReadOperation::Select => piv::select_application(options).map(Inner::Bytes), PivReadOperation::Version => piv::read_version_selected(options).map(Inner::Bytes), - PivReadOperation::AlgorithmConfiguration => { - piv::read_configuration_selected(options).map(Inner::Configuration) - } }; Self::from_operation(operation) } @@ -2870,6 +2909,11 @@ impl ProtocolOperation { } bytes(out) } + // Console exposes no serial/challenge-response operation; these + // outcomes cannot occur from the facade's request set. + oath::Outcome::Serial(_) | oath::Outcome::ChallengeResponse(_) => { + ProtocolStep::failure(Error::new(ErrorKind::InvalidResponse)) + } }), Some(Inner::OpenPgp(op)) => drive(op, response, |outcome| match outcome { openpgp::Outcome::Bytes(value) => bytes(value.as_bytes().to_vec()), @@ -2935,7 +2979,10 @@ impl ProtocolOperation { drive(op, response, |entries| bytes(ctap_rps_data(entries))) } Some(Inner::CtapCredentials(op)) => drive(op, response, |entries| { - bytes(ctap_credentials_data(entries)) + match ctap_credentials_data(entries) { + Ok(data) => bytes(data), + Err(error) => ProtocolStep::failure(error), + } }), None => Err(Error::new(ErrorKind::OperationStateError)), }; @@ -3341,11 +3388,13 @@ mod tests { } #[test] - fn configuration_uses_upstream_validation() { - let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + fn algorithm_config_read_reuses_selection_and_validates() { + // Without a management key the caller's selected transaction is reused. + let profile = metadata_profile(); + let mut op = profile.piv_read_algorithm_config(None, 0x03); assert_eq!(op.start().command.unwrap(), [0, 0xee, 1, 0, 0]); assert!(op.advance(vec![1, 0x90, 0]).error.is_some()); - let mut op = ProtocolOperation::piv_read(PivReadOperation::AlgorithmConfiguration); + let mut op = profile.piv_read_algorithm_config(None, 0x03); op.start(); let bytes = vec![1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3]; let mut response = bytes.clone(); @@ -3353,6 +3402,77 @@ mod tests { assert_eq!(op.advance(response).data.unwrap(), bytes); } + fn piv_profile(firmware: &[u8]) -> ProtocolProfile { + let mut observations = canokey::compatibility::DeviceObservations::new(firmware.to_vec()); + observations.piv_version = Some(canokey::compatibility::PivApplicationVersion([6, 0, 0])); + ProtocolProfile { + inner: Some(canokey::DeviceProfile::from_observations(observations).unwrap()), + } + } + + #[test] + fn algorithm_config_read_capability_gate_precedes_authentication() { + // Firmware without the INS EE read fails at construction with zero I/O, + // with or without a management key: no SELECT and no GA is emitted. + let profile = piv_profile(b"2.0.0"); + let mut op = profile.piv_read_algorithm_config(None, 0x03); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + let mut op = profile.piv_read_algorithm_config(Some(vec![0; 24]), 0x03); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedFeature"); + } + + #[test] + fn algorithm_config_read_authenticates_management_on_gated_firmware() { + // 3.0.x gates the read behind management-key authentication; a supplied + // key makes upstream SELECT, authenticate and read in one operation. + let profile = piv_profile(b"3.0.3"); + let key = vec![ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, + 0xef, 0x01, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, + ]; + let mut op = profile.piv_read_algorithm_config(Some(key), 0x03); + assert_eq!( + op.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xa0, 0, 0, 3, 8, 0] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0x87, 3, 0x9b, 4, 0x7c, 2, 0x81, 0, 0] + ); + let challenge = [ + 0x7c, 0x0a, 0x81, 0x08, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0x90, 0, + ]; + assert_eq!( + op.advance(challenge.to_vec()).command.unwrap(), + [ + 0, 0x87, 3, 0x9b, 0x0c, 0x7c, 0x0a, 0x82, 0x08, 0x07, 0x37, 0xf6, 0xc5, 0x37, 0x50, + 0xd4, 0xa4, 0 + ] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xee, 1, 0, 0] + ); + let bytes = vec![1, 0xe0, 5, 0x16, 0xe1, 0x53, 0x15, 0x54, 0xe2, 0xe3]; + let mut response = bytes.clone(); + response.extend([0x90, 0]); + assert_eq!(op.advance(response).data.unwrap(), bytes); + + // An unauthenticated Existing read is constructible; the card-side gate + // answers 6982, which stays a security failure. + let mut op = profile.piv_read_algorithm_config(None, 0x03); + assert_eq!(op.start().command.unwrap(), [0, 0xee, 1, 0, 0]); + let error = op.advance(vec![0x69, 0x82]).error.unwrap(); + assert_eq!(error.kind, "SecurityStatusNotSatisfied"); + assert_eq!(error.phase, "Command"); + + // Unknown key algorithms and malformed keys fail before any I/O. + let mut op = profile.piv_read_algorithm_config(Some(vec![0; 24]), 0xff); + assert_eq!(op.start().error.unwrap().kind, "UnsupportedAlgorithm"); + let mut op = profile.piv_read_algorithm_config(Some(vec![0; 8]), 0x03); + assert!(op.start().error.is_some()); + } + fn oath_selection(protected_: bool) -> Vec { let mut data = vec![0x79, 3, 6, 0, 0, 0x71, 8]; data.extend(b"12345678"); @@ -3840,6 +3960,17 @@ mod tests { assert_eq!(error.phase, "Parsing"); } + #[test] + fn pass_slots_bare_read_fails_at_construction_without_io() { + // INS 43 sits behind the firmware Admin-PIN gate on every audited + // firmware, so upstream rejects a bare read before any exchange. + let profile = metadata_profile(); + let mut op = profile.admin_pass_slots(None, false); + let error = op.start().error.unwrap(); + assert_eq!(error.kind, "SecurityStatusNotSatisfied"); + assert!(op.start().command.is_none()); + } + #[test] fn admin_existing_never_selects_or_verifies_and_rejects_pin() { let profile = metadata_profile(); @@ -4358,6 +4489,26 @@ mod tests { ) } + #[test] + fn ctap_status_bytes_map_through_application_status() { + // Upstream keeps CTAP applet status bytes in application_status and + // reserves status_word for ISO 7816; the bridge keeps the Dart + // contract by widening application_status into status_word. + let mut error = Error::new(ErrorKind::InvalidPin).at(Phase::Command); + error.application_status = Some(0x31); + assert_eq!(ProtocolError::from(error).status_word, Some(0x31)); + // The ISO status-word path is unchanged. + let error = Error::status( + canokey_protocol::StatusWord::new(0x6a82), + Phase::Command, + None, + ); + assert_eq!(ProtocolError::from(error).status_word, Some(0x6a82)); + // Neither status present stays absent. + let error = Error::new(ErrorKind::InvalidResponse); + assert_eq!(ProtocolError::from(error).status_word, None); + } + #[test] fn ctap_get_info_parses_fields_and_keeps_ctap_status() { let mut op = ProtocolOperation::ctap_get_info(); @@ -4383,7 +4534,8 @@ mod tests { let data = op.advance(ctap_ok(&minimal)).data.unwrap(); assert_eq!(data, [0, 0, 0, 0, 0]); - // A CTAP-level failure keeps the raw CTAP status byte, not an ISO SW. + // A CTAP-level failure keeps the raw CTAP status byte (upstream carries + // it in application_status; the bridge maps it to status_word). let mut op = ProtocolOperation::ctap_get_info(); ctap_after_select(&mut op); let error = op.advance(vec![0x30, 0x90, 0x00]).error.unwrap(); @@ -4458,7 +4610,7 @@ mod tests { Vec::::new() ); - // PIN_INVALID keeps the raw CTAP status byte in status_word. + // PIN_INVALID keeps the raw CTAP status byte via application_status. let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); let mut op = session.change_pin(b"1234".to_vec(), b"654321".to_vec()); ctap_after_select(&mut op); @@ -4516,7 +4668,7 @@ mod tests { let mut op = session.get_pin_token_with_permissions(vec![0xff, 0xfe, 0x41, 0x42], 4, None); assert_eq!(op.start().error.unwrap().kind, "InvalidPin"); - // PIN_BLOCKED keeps the raw CTAP status byte. + // PIN_BLOCKED keeps the raw CTAP status byte via application_status. let session = ctap_fixture_session(ctap::PinUvAuthProtocol::V1); let mut op = session.get_pin_token_with_permissions(b"1234".to_vec(), 4, None); ctap_after_select(&mut op); @@ -4645,7 +4797,8 @@ mod tests { Vec::::new() ); - // An unknown credential is NotFound with the raw CTAP status byte. + // An unknown credential is NotFound with the raw CTAP status byte + // carried through application_status. let mut op = token.delete_credential(vec![1, 2, 3, 4]); ctap_after_select(&mut op); let error = op.advance(vec![0x2e, 0x90, 0x00]).error.unwrap(); diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index a084531..a7774e0 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.13.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1993561274; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -729938; // Section: executor @@ -4309,6 +4309,60 @@ fn wire__crate__api__protocol__ProtocolProfile_piv_move_key_impl( }, ) } +fn wire__crate__api__protocol__ProtocolProfile_piv_read_algorithm_config_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ProtocolProfile_piv_read_algorithm_config", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_management_key = >>::sse_decode(&mut deserializer); + let api_management_key_algorithm = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Ok::<_, ()>( + crate::api::protocol::ProtocolProfile::piv_read_algorithm_config( + &*api_that_guard, + api_management_key, + api_management_key_algorithm, + ), + )?; + std::result::Result::Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -6087,8 +6141,7 @@ impl SseDecode for crate::api::protocol::PivReadOperation { return match inner { 0 => crate::api::protocol::PivReadOperation::Select, 1 => crate::api::protocol::PivReadOperation::Version, - 2 => crate::api::protocol::PivReadOperation::AlgorithmConfiguration, - 3 => crate::api::protocol::PivReadOperation::PinStatus, + 2 => crate::api::protocol::PivReadOperation::PinStatus, _ => unreachable!("Invalid variant for PivReadOperation: {}", inner), }; } @@ -6631,98 +6684,103 @@ fn pde_ffi_dispatcher_sync_impl( rust_vec_len, data_len, ), - 86 => wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( + 86 => wire__crate__api__protocol__ProtocolProfile_piv_read_algorithm_config_impl( + ptr, + rust_vec_len, + data_len, + ), + 87 => wire__crate__api__protocol__ProtocolProfile_piv_read_object_impl( ptr, rust_vec_len, data_len, ), - 87 => wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( + 88 => wire__crate__api__protocol__ProtocolProfile_piv_reset_pin_puk_retries_impl( ptr, rust_vec_len, data_len, ), - 88 => wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( + 89 => wire__crate__api__protocol__ProtocolProfile_piv_set_algorithm_config_impl( ptr, rust_vec_len, data_len, ), - 89 => wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( + 90 => wire__crate__api__protocol__ProtocolProfile_piv_set_container_name_impl( ptr, rust_vec_len, data_len, ), - 90 => wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( + 91 => wire__crate__api__protocol__ProtocolProfile_piv_set_management_key_impl( ptr, rust_vec_len, data_len, ), - 91 => { + 92 => { wire__crate__api__protocol__ProtocolProfile_piv_sign_impl(ptr, rust_vec_len, data_len) } - 92 => wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( + 93 => wire__crate__api__protocol__ProtocolProfile_piv_sign_streaming_impl( ptr, rust_vec_len, data_len, ), - 93 => wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( + 94 => wire__crate__api__protocol__ProtocolProfile_piv_sm2_agreement_impl( ptr, rust_vec_len, data_len, ), - 94 => wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( + 95 => wire__crate__api__protocol__ProtocolProfile_piv_write_object_impl( ptr, rust_vec_len, data_len, ), - 95 => wire__crate__api__protocol__ProtocolProfile_serial_impl(ptr, rust_vec_len, data_len), - 96 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), - 97 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), - 98 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( + 96 => wire__crate__api__protocol__ProtocolProfile_serial_impl(ptr, rust_vec_len, data_len), + 97 => wire__crate__api__piv_crypto__build_piv_public_key_impl(ptr, rust_vec_len, data_len), + 98 => wire__crate__api__decode__decode_png_qrcode_impl(ptr, rust_vec_len, data_len), + 99 => wire__crate__api__crypto__encrypt_piv_management_key_challenge_impl( ptr, rust_vec_len, data_len, ), - 99 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), - 100 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( + 100 => wire__crate__api__piv_crypto__finish_piv_csr_impl(ptr, rust_vec_len, data_len), + 101 => wire__crate__api__piv_crypto__finish_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 101 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), - 102 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), - 103 => { + 102 => wire__crate__api__crypto__hmac_sha1_impl(ptr, rust_vec_len, data_len), + 103 => wire__crate__api__init_app_impl(ptr, rust_vec_len, data_len), + 104 => { wire__crate__api__piv_crypto__parse_piv_import_file_impl(ptr, rust_vec_len, data_len) } - 104 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( + 105 => wire__crate__api__piv_crypto__parse_piv_public_key_info_impl( ptr, rust_vec_len, data_len, ), - 105 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), - 106 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), - 107 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), - 108 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( + 106 => wire__crate__api__crypto__parse_x509_cert_from_der_impl(ptr, rust_vec_len, data_len), + 107 => wire__crate__api__crypto__parse_x509_cert_from_pem_impl(ptr, rust_vec_len, data_len), + 108 => wire__crate__api__crypto__pbkdf2_hmac_sha1_impl(ptr, rust_vec_len, data_len), + 109 => wire__crate__api__piv_crypto__piv_certificate_supports_macos_impl( ptr, rust_vec_len, data_len, ), - 109 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), - 110 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( + 110 => wire__crate__api__piv_crypto__prepare_piv_csr_impl(ptr, rust_vec_len, data_len), + 111 => wire__crate__api__piv_crypto__prepare_piv_signing_input_impl( ptr, rust_vec_len, data_len, ), - 111 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( + 112 => wire__crate__api__piv_crypto__prepare_self_signed_certificate_impl( ptr, rust_vec_len, data_len, ), - 112 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), - 113 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), - 114 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), - 115 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), - 116 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), - 117 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), + 113 => wire__crate__api__crypto__sha256_digest_impl(ptr, rust_vec_len, data_len), + 114 => wire__crate__api__crypto__sha384_digest_impl(ptr, rust_vec_len, data_len), + 115 => wire__crate__api__crypto__sha512_digest_impl(ptr, rust_vec_len, data_len), + 116 => wire__crate__api__crypto__sm2_message_digest_impl(ptr, rust_vec_len, data_len), + 117 => wire__crate__api__crypto__tdes_ede3_enc_impl(ptr, rust_vec_len, data_len), + 118 => wire__crate__api__crypto__verify_piv_signature_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7071,8 +7129,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::protocol::PivReadOperation { match self { Self::Select => 0.into_dart(), Self::Version => 1.into_dart(), - Self::AlgorithmConfiguration => 2.into_dart(), - Self::PinStatus => 3.into_dart(), + Self::PinStatus => 2.into_dart(), _ => unreachable!(), } } @@ -7635,8 +7692,7 @@ impl SseEncode for crate::api::protocol::PivReadOperation { match self { crate::api::protocol::PivReadOperation::Select => 0, crate::api::protocol::PivReadOperation::Version => 1, - crate::api::protocol::PivReadOperation::AlgorithmConfiguration => 2, - crate::api::protocol::PivReadOperation::PinStatus => 3, + crate::api::protocol::PivReadOperation::PinStatus => 2, _ => { unimplemented!(""); } diff --git a/test/helper/utils/pass_card_test.dart b/test/helper/utils/pass_card_test.dart index 63306d8..25ec7e4 100644 --- a/test/helper/utils/pass_card_test.dart +++ b/test/helper/utils/pass_card_test.dart @@ -21,15 +21,17 @@ void main() { 'reads typed slots, preserving OATH references and unknown states', () async { final transport = _Transport([ + '9000', '9000', // Short = static with Enter; long = OATH credential "test" without Enter. '0201010474657374009000', '9000', + '9000', // Short = unknown type 0x7F carried verbatim; long = off. '7F009000', ]); await _prepared(transport, (client) async { - final slots = await client.readSlots(); + final slots = await client.readSlots(pin: '123456'); expect(slots, hasLength(2)); expect(slots[0].type, PassSlotType.static); expect(slots[0].withEnter, isTrue); @@ -37,22 +39,36 @@ void main() { expect(slots[1].name, 'test'); expect(slots[1].withEnter, isFalse); - final unknown = await client.readSlots(); + final unknown = await client.readSlots(pin: '123456'); expect(unknown[0].type, PassSlotType.unknown); expect(unknown[1].type, PassSlotType.none); - // A PIN-less read SELECTs but never verifies. + // INS 43 is PIN-gated on every audited firmware: each read without + // recorded session evidence SELECTs and verifies its explicit PIN. expect(transport.commands, [ _select, + _verify, '0043000000', _select, + _verify, '0043000000', ]); }); }, ); - test('reads verify the supplied PIN on gated firmware', () async { + test('a bare read fails at construction without any I/O', () async { + final transport = _Transport([]); + await _prepared(transport, (client) async { + await expectLater( + client.readSlots(), + _kind('SecurityStatusNotSatisfied'), + ); + expect(transport.commands, isEmpty); + }); + }); + + test('reads verify the supplied PIN', () async { final transport = _Transport([ '9000', '9000', @@ -67,10 +83,13 @@ void main() { }); test('malformed slot dumps fail instead of fabricating slots', () async { - final transport = _Transport(['9000', '029000']); + final transport = _Transport(['9000', '9000', '029000']); await _prepared(transport, (client) async { - await expectLater(client.readSlots(), _kind('InvalidResponse')); - expect(transport.commands, [_select, '0043000000']); + await expectLater( + client.readSlots(pin: '123456'), + _kind('InvalidResponse'), + ); + expect(transport.commands, [_select, _verify, '0043000000']); }); }); diff --git a/test/helper/utils/piv_card_test.dart b/test/helper/utils/piv_card_test.dart index 31d9ccd..371aff1 100644 --- a/test/helper/utils/piv_card_test.dart +++ b/test/helper/utils/piv_card_test.dart @@ -649,12 +649,14 @@ void main() { 'algorithm configuration propagates security and malformed replies', () async { for (final response in ['6982', '6F00', '019000']) { - final transport = _QueueApduTransport(['9000', response]); - await expectLater( - PivCardClient(transport: transport).readAlgorithmExtensions(), - throwsA(isA()), - ); - expect(transport.commands, ['00A4040005A00000030800', '00EE010000']); + final transport = _QueueApduTransport([response]); + await _withPreparedClient(transport, (client) async { + await expectLater( + client.readAlgorithmExtensions(), + throwsA(isA()), + ); + expect(transport.commands, ['00EE010000']); + }); } }, ); @@ -662,55 +664,56 @@ void main() { test( 'algorithm configuration authenticates the management key when supplied', () async { - // 3.0.x firmware gates the read behind management-key authentication. - const firmware = '3.0.3'; - final probe = [..._probeResponses]; - probe[1] = '${hex.encode(firmware.codeUnits)}9000'; + // 3.0.x firmware gates the read behind management-key authentication; + // upstream SELECTs, authenticates and reads within one operation. final transport = _QueueApduTransport([ - '9000', // explicit SELECT - ...probe, + '9000', // SELECT PIV '7C0A8108FEDCBA98765432109000', // management challenge '9000', // management authentication response '01E00516E1531554E2E39000', // extension configuration ]); - final client = PivCardClient(transport: transport); - await client.withSession(() async { + await _withPreparedClient(transport, (client) async { final config = await client.readAlgorithmExtensions( managementKey: '0123456789abcdef23456789abcdef01456789abcdef0123', ); expect(config, isNotNull); expect(config!.enabled, isTrue); - // No SELECT between the authentication and the gated read. + // One SELECT, then the authentication exchange and the gated read. expect(transport.commands, [ '00A4040005A00000030800', - '00A4040005F00000000000', - '0031000000', - '0031010000', - '0032000000', - '00A4040005A00000030800', - '00FD000000', - '00EE010000', '0087039B047C02810000', '0087039B0C7C0A82080737F6C53750D4A400', '00EE010000', ]); - }); + }, firmware: '3.0.3'); }, ); test('reads optional algorithm extensions and missing metadata', () async { - final config = await PivCardClient( - transport: _QueueApduTransport(['9000', '01E00516E1531554E2E39000']), - ).readAlgorithmExtensions(); - expect(config, isNotNull); - expect(config!.enabled, isTrue); - - expect( - await PivCardClient( - transport: _QueueApduTransport(['9000', '6D00']), - ).readAlgorithmExtensions(), - isNull, + await _withPreparedClient( + _QueueApduTransport(['01E00516E1531554E2E39000']), + (client) async { + final config = await client.readAlgorithmExtensions(); + expect(config, isNotNull); + expect(config!.enabled, isTrue); + }, ); + + // A card-side unsupported instruction keeps the null fallback. + final unsupported = _QueueApduTransport(['6D00']); + await _withPreparedClient(unsupported, (client) async { + expect(await client.readAlgorithmExtensions(), isNull); + expect(unsupported.commands, ['00EE010000']); + }); + + // Firmware without the read fails the capability check at construction: + // zero I/O, same null fallback. + final legacy = _QueueApduTransport([]); + await _withPreparedClient(legacy, (client) async { + expect(await client.readAlgorithmExtensions(), isNull); + expect(legacy.commands, isEmpty); + }, firmware: '2.0.0'); + expect( await _withPreparedClient( _QueueApduTransport(['6A88']), diff --git a/test/usbip/console_smoke.dart b/test/usbip/console_smoke.dart index 581d293..7a1a886 100644 --- a/test/usbip/console_smoke.dart +++ b/test/usbip/console_smoke.dart @@ -773,13 +773,12 @@ class ConsoleSmoke { } } - // Firmware 3.0.x gates the extension-config read behind management-key - // authentication; newer firmware allows it unauthenticated. - final gatesExtensionRead = - firmware.compareTo(const FirmwareVersion(3, 0, 0)) >= 0 && - firmware.compareTo(const FirmwareVersion(3, 1, 0)) < 0; + // The upstream capability check runs at construction, before any + // authentication, so firmware without the extension read fails without + // a GENERAL AUTHENTICATE; on 3.0.x the default key authorizes the + // management-gated read within the same operation. await _pivClient.readAlgorithmExtensions( - managementKey: gatesExtensionRead ? _defaultPivManagementKey : null, + managementKey: _defaultPivManagementKey, ); stdout.writeln('ok: piv.client.read_change_and_restore'); }); From 66271c78a5255cfdcb46a04797d76f061f32c806 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 19:35:46 +0800 Subject: [PATCH 07/19] chore: unify wasm-bindgen family at 0.2.128 and refresh dependencies The fido2-era exact pins are gone; the wasm-bindgen family now tracks the wasm-bindgen-cli used by the FRB web build, so build-web works with the stock toolchain and no host-side shim. Also bumps semver-compatible dependencies and regenerates THIRD_PARTY_LICENSES.json. --- README.md | 6 +- rust/Cargo.lock | 172 +++++++++++++++++++-------------- rust/Cargo.toml | 10 +- rust/THIRD_PARTY_LICENSES.json | 92 +++++++++++------- 4 files changed, 168 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 57689b9..88e10cd 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,11 @@ Visit our web application at [CanoKey Console Web](https://console.canokeys.org) For Chrome development, run `flutter run -d chrome --cross-origin-isolation` to enable the headers required for shared WASM memory. -If `wasm-pack` cannot download its helper and fails while compiling +The `wasm-bindgen` crate family is pinned in `rust/Cargo.toml` and the +`wasm-bindgen-cli` used by `wasm-pack` must match it (currently 0.2.128). +If `wasm-pack` cannot reuse an installed helper and fails while compiling `wasm-bindgen-cli` with WASM linker flags on the host, install the matching -helper separately with `cargo +stable install wasm-bindgen-cli --version 0.2.100 --locked` +helper separately, e.g. `cargo +stable install wasm-bindgen-cli --version 0.2.128 --locked`, and ensure its `bin` directory is on `PATH` before rebuilding. After changing Rust code or regenerating Flutter Rust Bridge bindings, rebuild diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3179e43..ed24571 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -279,9 +279,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "bitstream-io" @@ -483,9 +483,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -549,7 +549,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "inout 0.1.4", "zeroize", ] @@ -613,6 +613,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpubits" version = "0.1.1" @@ -639,18 +645,18 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -658,18 +664,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -707,9 +713,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -837,9 +843,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid 0.10.2", "der_derive 0.8.0", @@ -916,7 +922,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] @@ -961,7 +967,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der 0.8.1", + "der 0.8.2", "digest 0.11.3", "elliptic-curve 0.14.1", "rfc6979 0.6.0", @@ -1045,11 +1051,17 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -1318,9 +1330,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1468,9 +1480,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "subtle", "typenum", @@ -1626,11 +1638,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1791,6 +1804,33 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "nalgebra" version = "0.32.6" @@ -2180,7 +2220,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", ] @@ -2588,7 +2628,7 @@ dependencies = [ "canokey", "canokey-protocol", "const-oid 0.10.2", - "der 0.8.1", + "der 0.8.2", "des 0.9.0", "ed25519-dalek", "flutter_rust_bridge", @@ -2713,7 +2753,7 @@ checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct 1.0.0", "ctutils", - "der 0.8.1", + "der 0.8.2", "hybrid-array", "subtle", "zeroize", @@ -2871,6 +2911,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -2916,9 +2962,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "spin" @@ -2943,7 +2989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der 0.8.2", ] [[package]] @@ -3232,48 +3278,32 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3281,31 +3311,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", - "wasm-bindgen-backend", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -3414,7 +3444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" dependencies = [ "const-oid 0.10.2", - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", "tls_codec", ] @@ -3438,18 +3468,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -3478,9 +3508,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zune-core" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 119ea67..94ae293 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -37,11 +37,11 @@ x509-parser = "^0.18.1" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "^0.4.3", features = ["wasm_js"] } -# Keep the wasm-bindgen family pinned for reproducible FRB web builds. -wasm-bindgen = "=0.2.100" -js-sys = "=0.3.77" -web-sys = "=0.3.77" -wasm-bindgen-futures = "=0.4.50" +# wasm-bindgen family tracks the wasm-bindgen-cli used by the FRB web build. +wasm-bindgen = "=0.2.128" +js-sys = "=0.3.105" +web-sys = "=0.3.105" +wasm-bindgen-futures = "=0.4.78" [dev-dependencies] diff --git a/rust/THIRD_PARTY_LICENSES.json b/rust/THIRD_PARTY_LICENSES.json index 1b8fd9b..dcad422 100644 --- a/rust/THIRD_PARTY_LICENSES.json +++ b/rust/THIRD_PARTY_LICENSES.json @@ -181,7 +181,7 @@ }, { "package_name": "bitflags", - "package_version": "2.13.1", + "package_version": "2.13.2", "repository": "https://github.com/bitflags/bitflags", "license": "MIT OR Apache-2.0" }, @@ -383,6 +383,12 @@ "repository": "https://github.com/servo/core-foundation-rs", "license": "MIT OR Apache-2.0" }, + { + "package_name": "core_detect", + "package_version": "1.0.0", + "repository": "https://github.com/thomcc/core_detect", + "license": "MIT/Apache-2.0" + }, { "package_name": "cpubits", "package_version": "0.1.1", @@ -403,25 +409,25 @@ }, { "package_name": "crc32fast", - "package_version": "1.5.1", + "package_version": "1.5.2", "repository": "https://github.com/srijs/rust-crc32fast", "license": "MIT OR Apache-2.0" }, { "package_name": "crossbeam-deque", - "package_version": "0.8.7", + "package_version": "0.8.8", "repository": "https://github.com/crossbeam-rs/crossbeam", "license": "MIT OR Apache-2.0" }, { "package_name": "crossbeam-epoch", - "package_version": "0.9.20", + "package_version": "0.9.21", "repository": "https://github.com/crossbeam-rs/crossbeam", "license": "MIT OR Apache-2.0" }, { "package_name": "crossbeam-utils", - "package_version": "0.8.22", + "package_version": "0.8.23", "repository": "https://github.com/crossbeam-rs/crossbeam", "license": "MIT OR Apache-2.0" }, @@ -445,7 +451,7 @@ }, { "package_name": "crypto-common", - "package_version": "0.1.6", + "package_version": "0.1.7", "repository": "https://github.com/RustCrypto/traits", "license": "MIT OR Apache-2.0" }, @@ -505,7 +511,7 @@ }, { "package_name": "der", - "package_version": "0.8.1", + "package_version": "0.8.2", "repository": "https://github.com/RustCrypto/formats", "license": "Apache-2.0 OR MIT" }, @@ -607,7 +613,7 @@ }, { "package_name": "encoding_rs", - "package_version": "0.8.35", + "package_version": "0.8.41", "repository": "https://github.com/hsivonen/encoding_rs", "license": "(Apache-2.0 OR MIT) AND BSD-3-Clause" }, @@ -763,7 +769,7 @@ }, { "package_name": "generic-array", - "package_version": "0.14.9", + "package_version": "0.14.7", "repository": "https://github.com/fizyk20/generic-array.git", "license": "MIT" }, @@ -865,7 +871,7 @@ }, { "package_name": "hybrid-array", - "package_version": "0.4.14", + "package_version": "0.4.15", "repository": "https://github.com/RustCrypto/hybrid-array", "license": "MIT OR Apache-2.0" }, @@ -937,8 +943,8 @@ }, { "package_name": "js-sys", - "package_version": "0.3.77", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys", + "package_version": "0.3.105", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys", "license": "MIT OR Apache-2.0" }, { @@ -1049,6 +1055,24 @@ "repository": "https://github.com/awxkee/moxcms.git", "license": "BSD-3-Clause OR Apache-2.0" }, + { + "package_name": "multiversion", + "package_version": "0.9.0", + "repository": "https://github.com/calebzulawski/multiversion", + "license": "MIT OR Apache-2.0" + }, + { + "package_name": "multiversion-macros", + "package_version": "0.9.0", + "repository": "https://github.com/calebzulawski/multiversion", + "license": "MIT OR Apache-2.0" + }, + { + "package_name": "multiversion_no_op", + "package_version": "1.0.0", + "repository": "https://github.com/hsivonen/multiversion_no_op", + "license": "Apache-2.0 OR MIT" + }, { "package_name": "nalgebra", "package_version": "0.32.6", @@ -1649,6 +1673,12 @@ "repository": "https://github.com/lu-zero/simd_helpers", "license": "MIT" }, + { + "package_name": "simdutf8", + "package_version": "0.1.5", + "repository": "https://github.com/rusticstuff/simdutf8", + "license": "MIT OR Apache-2.0" + }, { "package_name": "siphasher", "package_version": "1.0.3", @@ -1681,7 +1711,7 @@ }, { "package_name": "smallvec", - "package_version": "1.16.0", + "package_version": "1.16.1", "repository": "https://github.com/servo/rust-smallvec", "license": "MIT OR Apache-2.0" }, @@ -1885,44 +1915,38 @@ }, { "package_name": "wasm-bindgen", - "package_version": "0.2.100", - "repository": "https://github.com/rustwasm/wasm-bindgen", - "license": "MIT OR Apache-2.0" - }, - { - "package_name": "wasm-bindgen-backend", - "package_version": "0.2.100", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/backend", + "package_version": "0.2.128", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen", "license": "MIT OR Apache-2.0" }, { "package_name": "wasm-bindgen-futures", - "package_version": "0.4.50", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures", + "package_version": "0.4.78", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/futures", "license": "MIT OR Apache-2.0" }, { "package_name": "wasm-bindgen-macro", - "package_version": "0.2.100", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro", + "package_version": "0.2.128", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro", "license": "MIT OR Apache-2.0" }, { "package_name": "wasm-bindgen-macro-support", - "package_version": "0.2.100", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/macro-support", + "package_version": "0.2.128", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/main/crates/macro-support", "license": "MIT OR Apache-2.0" }, { "package_name": "wasm-bindgen-shared", - "package_version": "0.2.100", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/shared", + "package_version": "0.2.128", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared", "license": "MIT OR Apache-2.0" }, { "package_name": "web-sys", - "package_version": "0.3.77", - "repository": "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys", + "package_version": "0.3.105", + "repository": "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/web-sys", "license": "MIT OR Apache-2.0" }, { @@ -1999,13 +2023,13 @@ }, { "package_name": "zerocopy", - "package_version": "0.8.56", + "package_version": "0.8.57", "repository": "https://github.com/google/zerocopy", "license": "BSD-2-Clause OR Apache-2.0 OR MIT" }, { "package_name": "zerocopy-derive", - "package_version": "0.8.56", + "package_version": "0.8.57", "repository": "https://github.com/google/zerocopy", "license": "BSD-2-Clause OR Apache-2.0 OR MIT" }, @@ -2023,7 +2047,7 @@ }, { "package_name": "zlib-rs", - "package_version": "0.6.7", + "package_version": "0.6.8", "repository": "https://github.com/trifectatechfoundation/zlib-rs", "license": "Zlib" }, From e7f77fae7794fe7a01e051f2b1a38e74d232bd35 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 19:49:26 +0800 Subject: [PATCH 08/19] refactor: adopt libcanokey 41a3ea60 and rewrite the integration doc NDEF now honors the CC-advertised file ID and preflights writes against the capability container; PASS requests are gated behind Capability::AdminPassConfig. The migration doc is rewritten as a current-state reference; per-increment history lives in git. --- docs/libcanokey-migration.md | 726 ++++---------------------- lib/helper/utils/ndef_card.dart | 13 +- rust/Cargo.lock | 20 +- rust/Cargo.toml | 4 +- rust/src/api/protocol.rs | 42 +- test/helper/utils/ndef_card_test.dart | 31 +- 6 files changed, 177 insertions(+), 659 deletions(-) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index cff59cc..94277b3 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -1,634 +1,106 @@ -# libcanokey migration - -The first increment uses the Rust facade through Flutter Rust Bridge, following -[canokey-pkcs11#5](https://github.com/canokeys/canokey-pkcs11/pull/5). Cargo pins -libcanokey to the `9110bc71a0b00f3517b96651ed63e030f8fffadc` revision (with the -facade's opt-in `clientpin` feature). -The C ABI is unnecessary in Console's existing Rust library. - -## Implemented - -- PIV selection uses `select_application`, including an explicit Le for legacy - readers. This is still an explicit caller action and may clear authentication. -- PIV version and algorithm configuration reads use the upstream selected-app - operations. Neither inserts SELECT, probes the device, or authenticates. -- PIV certificate reads use `read_certificate` with `Access::Existing` and an - explicitly prepared, lease-owned profile. The former local certificate machine - has been removed. libcanokey owns GET DATA, container validation and bounded gzip - decompression; no SELECT/probe/authentication is inserted. Only 6A82/6A88 return - a missing object; malformed certificates remain visible as invalid UI slots. -- PIV management-protection object reads/writes use `read_object`/`write_object` - with `Access::Existing`. The controller no longer builds GET/PUT DATA or unwraps - outer 53 containers. Missing objects alone become null; malformed/security - failures propagate. Upstream owns firmware checks, framing and segmentation. - Writes are never resumed or rolled back after partial or uncertain outcomes. -- An opaque FRB operation owns its protocol state across Dart await points. - The Dart executor exchanges complete APDUs, including response status words. - libcanokey owns continuation, safe Le correction, budgets and validation. -- Completion, protocol failures, malformed transport hex and transport exceptions - all close/dispose the operation. No mutation is retried or rolled back. - Byte buffers copied to Dart are wiped after each exchange; immutable transport - strings are not erasable by this executor. Console's physical transport retains - the existing full `C-APDU`/`R-APDU` diagnostic logs, including for lease-based - operations. These diagnostic copies are not erased by operation cleanup. -- Explicit `prepare()` probes Admin/PIV before metadata workflows authenticate. - The resulting opaque profile belongs to that physical lease and is closed on - rebinding or lease release. Once re-probing starts, failure cannot leave an older - profile usable; competing preparation is rejected before touching an active - operation's profile. -- PIV key/PIN/PUK/management metadata now uses `get_metadata` with `Access::Existing` and - upstream capability/slot checks, continuation and field validation. Reads never - SELECT, probe or authenticate. Only upstream `NotFound` (including supported - legacy 6900 empty-key responses) becomes null; security, unsupported-command, - invalid-length and malformed-field errors remain visible. UI mapping rejects - incomplete/unknown required display fields instead of inventing retries or - policies. Key algorithm display resolves through this device's profile, ignoring - stale UI extension IDs; display enum IDs never authorize a device operation. -- PIN verification/change, PUK change, PIN unblocking and explicit logout now use - `credential(select=false)`. Inputs enter upstream zeroizing owners; Dart wipes its - mutable input copies. Credential validation and legacy explicit Le follow the - pinned library. PIN/PUK failures preserve their reference and remaining retries; - only authentication rejection/blocked credentials map to the UI's false result. - Successful credential operations have `Unchanged` profile effect and do not - create a reusable authorization token. -- Management-key external authentication now uses - `authenticate_management_key(select=false)`: libcanokey owns both challenge APDUs, - parsing and 3DES/AES cryptography, with current-profile capability checks. - The old Dart challenge builders/parser were removed. This preserves external - mode; it does not claim mutual device authentication. -- Selected PIN status uses upstream `command::pin_status`, the operation engine - and error classifier. A small adapter retains the validated status word because - the pinned high-level `get_pin_status` inserts SELECT, which would disturb an - already authenticated transaction. Unexpected statuses and nonempty replies are - errors; 9000 never invents a retry count. Serial reads now return the optional - Admin serial observed by the explicit probe, without another applet switch. -- Explicit PUK blocking retains Console's bounded, intentional destructive loop, - with each CHANGE PUK executed upstream. Only expected credential outcomes allow - the next attempt; uncertain I/O, malformed responses and unexpected statuses - stop immediately. No mutation is replayed after a 6C response. -- Each executor reserves its lease across the entire operation, preventing raw - exchanges, another operation or rebind between continuation APDUs. -- Errors retain category, phase, status word, credential reference and retry count - as separate fields. Transport errors are propagated without conversion. -- Only an unsupported algorithm-configuration instruction permits the existing - firmware defaults, plus the management-gated read on 3.0.x: the upstream - `Capability::PivProtectedAlgorithmConfigRead` models that gate, so the - profile-based `pivReadAlgorithmConfig` binding rejects an unauthenticated - read with SecurityStatusNotSatisfied there (the card answers 6982), and the - unauthenticated capabilities read takes the defaults fallback on exactly - that kind, without a version check. `readAlgorithmExtensions` still accepts - an optional management key; upstream then SELECTs, authenticates and reads - within the same operation, and the capability check runs at construction - before any authentication, so firmware without the read never sees a - GENERAL AUTHENTICATE. Other security failures, malformed - responses and communication errors never silently choose default algorithms. - -- OpenPGP card-info reads, PW1/PW3 credential operations and all - administrative writes (reset code set/clear, retry limits, signature PIN - policy, touch policy/cache time, admin and reset-code unblocks) use upstream - `openpgp::operation` through the bridge. The client prepares with the minimal - Admin probe; the profile is immutable firmware evidence, and every upstream - operation SELECTs OpenPGP and explicitly verifies its own password. Only - `NotFound` makes optional cardholder/URL/touch-cache objects absent; the - pre-UIF firmware gate also stays absent without performing I/O. Credential - rejections, blocked passwords and target-stage security rejections map to - the UI's false result with the retained status word; other protocol and - transport failures propagate. `ResetRetries` resets PW1/PW3 to firmware - defaults on the card; Console caches no OpenPGP credentials, so no local - credential invalidation is required. -- OATH selection, access-code validation, put/delete/calculate/calculate-all - and code set/clear use upstream `oath::operation` through the bridge. The - FRB wrappers carry an optional access key plus fresh host challenge; each - operation SELECTs and validates within itself, so no authentication state is - cached or reused between operations. The controller's per-use-case - authentication only proves the code for prompting/cache policy. Legacy - (1.3.x) versus modern dialects come from the probed firmware profile, not - from parsing SELECT response TLVs; a legacy selection yields no synthetic - version or salt. Calculation results keep names, digit counts, raw truncated - bytes and HOTP/touch markers so display formatting (decimal, Steam) stays in - Dart. Wrong codes map `AuthenticationFailed`/`DeviceAuthenticationFailed` - (6A80 or a failed card proof) to the existing incorrect-code prompt; - duplicate-name 6985 and storage-full statuses keep their UI mapping; other - protocol and transport failures propagate without retry or replay. -- OATH set-default merges the former `setDefault`/`setDefaultLegacy` into one - `oathSetDefault` call; the legacy dialect is dispatched by the upstream - `Capability::OathSetDefaultSlots`. On legacy firmware, long slots and - appendEnter are rejected as `InvalidArgument` at construction with zero I/O - and the UI shows notSupported; a 6984 response maps NotFound to the - operationFailed prompt. `OathCardClient.transceive` and the dead - setDefaultLegacy path were removed; OATH no longer builds APDUs by hand. -- NDEF reads/writes use the profile-free `ndefReadCapability`/`ndefReadMessage`/ - `ndefWriteMessage` bindings; upstream owns SELECT, the 240-byte chunking and - the crash-safe three-phase write. A 6A82 on SELECT yields a null read / false - write capability, and 6982 maps to `NdefReadOnlyException`. All manual - chunking, NLEN and capability-container parsing was removed from - `ndef_card.dart`. -- CTAP/WebAuthn is fully upstream: the CTAP2 client-layer bindings cover every - WebAuthn management use case — `ctapGetInfo` (options tri-states, - forcePinChange, minPinLength and advertised pinUvAuthProtocol versions - parsed in Rust), ClientPIN `ctapBeginPinSession`/`setPin`/`changePin`/ - `getPinTokenWithPermissions`, and credentialManagement `enumerateRps`/ - `enumerateCredentials` (including the CanoKey metadataOnly 0x80 extension)/ - `deleteCredential`. PinSession and PinToken are opaque FRB handles holding - zeroizing Rust state; ephemeral scalars and V2 IVs are generated in the - facade from the CSPRNG (`rand`), never supplied by Dart. For CTAP-level - failures upstream carries the raw CTAP status byte in - `Error::application_status` (`status_word` is ISO-only again); the bridge - keeps the Dart contract by widening it into `ProtocolError.statusWord`, - which therefore is not an ISO 7816 status word for CTAP failures. Enumeration results use self-delimiting - byte encodings; Dart never parses CBOR. The Dart `WebAuthnCardClient` - (`lib/helper/utils/webauthn_card.dart`) decodes those encodings, prefers - pinUvAuthProtocol V2 when advertised, and owns session/token handle - lifecycles; every operation SELECTs the FIDO2 applet itself, so consecutive - operations in one use case never assume a residual selection. Tokens are - ceremony-scoped: each use case (enumeration, deletion) mints a fresh one - instead of caching the handle, and PIN set/change is followed by a fresh - getInfo. The Dart fido2 package, the `fido2_crypto` crate (and its C ABI in - `lib.rs`), the `web/fido2` WASM artifacts and the raw CTAP transmitter layer - were removed; no Dart CBOR/ClientPin framing remains. -- Pass reads/writes use `adminPassSlots` and - `adminSetPassSlot` with the same thin client shape as - admin_card; both reuse the lease's verified Admin session when one is - recorded and otherwise SELECT + verify an explicit per-request PIN. INS 43 - sits behind the firmware Admin-PIN gate on every audited firmware, so - upstream rejects a bare read (no recorded session, no PIN) at construction - with SecurityStatusNotSatisfied, before any I/O. Unknown slot types display - as unknown, and a write reporting `reprobeRequired` - invalidates the profile. The manual 0043/0044 commands were removed. -- New facade bindings in `rust/src/api/protocol.rs`: `adminPassSlots`/ - `adminSetPassSlot` (including the `AdminValueKind.passSlots` encoding), - `Access::Existing` support (the `existing` parameter) on the whole Admin - request path (`adminRead`/`adminConfigure`/`adminAction` and the PASS slot - operations), `pivImportPqSeed`, `pivReadAlgorithmConfig` (profile-based, - optional management key; replaces the profile-free - `PivReadOperation.algorithmConfiguration`), `oathSetDefault`, - `ndefReadCapability`/ - `ndefReadMessage`/`ndefWriteMessage` and `ctapSelectApplication`/ - `ctapTransceiveSelected`/`ctapTransceive`. The CTAP2 client-layer increment - adds `ctapGetInfo`, `ctapBeginPinSession`, the `CtapPinSession` methods - `protocolVersion`/`setPin`/`changePin`/`getPinTokenWithPermissions`/`close` - and the `CtapPinToken` methods `protocolVersion`/`enumerateRps`/ - `enumerateCredentials`/`deleteCredential`/`close`. - -Existing UI models still map validated configuration bytes to display/algorithm -fields. Admin coverage and the specific upstream gaps are described below. - -## Admin integration - -Admin uses an explicit minimal probe, which observes firmware/model/serial and does -not select PIV. Its profile belongs to the physical lease and is never fabricated -from UI firmware labels. Identity reads reuse those observations; other reads use -`admin::operation`, including version-dependent configuration parsing, chip ID, -core commit, NFC state, physical/logical storage and SM2 configuration. Only an -explicit `UnsupportedFeature` makes optional core-commit/applet-usage reads absent; -security, malformed replies and unknown capabilities remain failures. - -The pinned Admin facade SELECTs and verifies a PIN only when explicitly -supplied; with `Access::Existing` it sends neither and reuses the caller's -selected, already authorized transaction. Console records the evidence for -that reuse only on a successful explicit facade `VerifyPin` (itself -`existing: false`): the lease keeps an authentication stamp bound to its -selection and profile generations. Any applet selection (`willSelectApplet`), -profile invalidation, exposed or uncertain Admin mutation, failed Existing -request, or lease rebind/replacement expires the stamp, and later protected -requests fall back to `existing: false` plus an explicit per-request PIN. -The card remains the enforcement point: a wrongly assumed session surfaces as -a security rejection, which itself drops the stamp. The UI PIN cache stays -prompting/cache policy and never creates evidence. The controller's -authentication step still validates the PIN for prompting/cache policy and -leaves Admin selected for remaining legacy consumers. The settings refresh -skips its redundant re-probe while the prepared binding is still valid -(`prepareIfStale`), so one page load performs a single probe and a single -SELECT+VERIFY, with chip ID, core commit, configuration, NFC, storage and -keyboard reads reusing the session. The Pass page's explicit probe after -authentication re-selects Admin and therefore ends the evidence, so its -protected slot write keeps the per-request PIN sequence. Mutable PIN inputs -are wiped; the short-lived service copy belongs to the verified physical -lease and is cleared on rebind, release or controller disposal. A repolled -device's observed serial is checked before submitting a cached PIN. -Duplicate local/persisted PIN candidates are not automatically retried. -PIN change drops cached old credentials before the attempt; a failed or lost -acknowledgment does not restore them. Credential-cache invalidation -generations also expire page-local Admin/OATH/WebAuthn copies after reset or -PIN removal. Factory reset invalidates both saved and unsaved credentials -for that serial before the attempt; unrelated devices and preferences remain -intact. - -Configuration patches, PIN change, NFC/NDEF settings, historical keyboard flags and -OpenPGP touch, SM2 writes, applet resets and factory reset now use upstream requests. -Feature changes carry only the selected mask; libcanokey reads the current device -configuration, preserves unselected fields and refuses to overwrite unknown feature -bits. Factory reset submits no PIN guesses; blocked-PIN and presence requirements -remain card-enforced. The existing request to reset is not an automatic retry. - -The executor copies Admin progress before closing its handle, including after a -transport exception or cancellation. `lastProgress` retains acknowledged-write count -and `reprobeRequired`. Once a profile-affecting write is exposed, success, card -failure, malformed acknowledgment, cancellation and lost transport response all -invalidate profile evidence. Controllers explicitly prepare before a subsequent -operation; the executor never re-probes, retries, resumes or rolls back a mutation. -Multiple settings operations are not an atomic transaction. No-op patches need no -profile invalidation. A cancelled write whose acknowledgment was not processed is -exposed but unconfirmed, rather than falsely reported successful. - -Each lease now tracks selection and profile-evidence generations as well as its -physical connection generation. Admin SELECT invalidates previously prepared PIV -selection; Admin configuration writes invalidate profiles held by other clients on -the same lease. Injected transports can share an explicit caller-owned `CardLease` -for the same cross-applet contract. Unknown raw commands are not classified by -parsing APDUs in Dart; remaining legacy callers still own their selection boundary. - -Modern SM2 IDs use upstream signed big-endian validation and read/patch behavior. -For audited 3.0.x CanoKey devices, Console explicitly interprets the legacy packed -native i32 fields as little-endian and retains that encoding when editing them. -libcanokey validates the legacy nine-byte object without claiming portable byte -order. The result variant comes from the probed firmware, not guessed reply length. - -### Upstream gaps and remaining boundaries - -- **Admin selected-context operations:** converged. Protected Admin requests - reuse `Access::Existing` while the lease holds valid recorded session - evidence (see Admin integration above) and otherwise keep `existing: false` - with an explicit per-request PIN. `Request::VerifyPin` never uses Existing - (upstream rejects it), so the explicit authentication step is unchanged. -- **WebAuthn CBOR/ClientPin:** converged. The controller migrated off the - fido2 package onto the facade's CTAP2 client layer (`ctapGetInfo`, - `ctapBeginPinSession`, `setPin`/`changePin`/`getPinTokenWithPermissions`, - `enumerateRps`/`enumerateCredentials`/`deleteCredential`); no Dart-side - CBOR or ClientPin framing remains, and the `fido2_crypto` crate and its C - ABI were removed from `lib.rs`. -- **Keyboard keymap read/write/reset (45/46/47):** migrated; these commands - are routed through upstream Admin operations (see the existing-access - increment below). No Dart-side raw keymap exchanges remain. -- Connection-bootstrap identity APDUs in SmartCard now use the upstream - `admin::command::select`/`admin::command::serial` builders through the - facade's bootstrap binding, on the same exclusive queue as before. The - PIV macOS-setup serial read is the only remaining raw exchange in Dart. -- Firmware outside the pinned library's audited capability matrix remains - `CapabilityUnknown`/unsupported. Console does not bypass capability checks with - invented profiles or speculative wire formats. - -## Connection and operation lifecycle - -This increment follows upstream [Console integration](https://github.com/canokeys/libcanokey/blob/95e1930ea86eb248d0295bce9880cee5c65fd85e/docs/console-integration.md) -and [API contracts](https://github.com/canokeys/libcanokey/blob/95e1930ea86eb248d0295bce9880cee5c65fd85e/docs/api-design.md): - -1. `SmartCard.process` queues the entire use case, including connection setup, - SELECT, authentication, dependent commands, result publication and cleanup. - CCID connection/probe APDUs use the same queue; reader enumeration can observe - disconnection immediately and invalidate the active generation. -2. A lease captures the physical CCID handle or the polled NFC/WebUSB channel. - New polls explicitly bind a new generation. A closed, failed or replaced lease - cannot exchange or publish an executor result. A default SmartCard protocol - executor requires a `process` scope and captures one lease for its whole loop; - it never silently reacquires a connection between APDUs. -3. Dart owns the opaque Rust operation across each await. Generation/cancellation - checks occur before start/exchange, after I/O and before returning owned bytes. - Only the executor advances/closes/disposes its handle. PIV controller disposal - requests cancellation of migrated operations, without freeing their handles. -4. Cancellation waits for outstanding I/O to settle before releasing the use-case - lock. A transport exception poisons the lease and triggers awaited backend - disconnect/finish, without command replay. Failed cleanup quarantines the - channel until application restart. No Dart `Future.timeout` unlocks early. -5. PIN and forced-PIN-change prompt callbacks retain their owner zone across UI - events. Double submits cannot overlap exchanges. Explicit re-poll replaces the - old lease; closed dialog callbacks cannot revive a completed session. - -Operation close only releases local memory. It does not log out, reconnect, undo -writes or roll back card state. Injected transports in transcript tests still own -and isolate their fake connection; they can pass an explicit lease to the executor. -Standalone legacy raw exchanges respect the queue but do not establish a multi-APDU -transaction: production use cases must keep using `SmartCard.process`. This is -application-local exclusivity; it does not add an OS-level PC/SC transaction API -to the CCID plugin. - -`DeviceProfile` now lives only within a prepared physical lease; no profile or -access state is cached across use cases. PIV controller use cases explicitly prepare before dependent reads and any -authentication. An explicit SELECT invalidates the previous preparation. Raw -controller commands and selected reads also respect active operation reservations, -including with injected transports. -Injected transports use `PivCardClient.withSession` for this same ownership contract; -the USB/IP metadata smoke path has been updated accordingly. - -The profile is immutable evidence, not an authentication token. Metadata factories -use `Access::Existing` without asserting PIN or management authorization. Operations -copy their required profile evidence and survive source-profile close; Dart still -checks the original lease before every exchange and publication. Cancelled or stale -probe results are closed/disposed instead of being published. Lease cleanup releases -all registered resources even when one destructor fails, and always releases the -use-case queue. - -Credential and management authentication factories explicitly omit SELECT without -asserting prior authorization. There is no host authentication cache. Uncertain -credential/authentication outcomes invalidate preparation; callers must not continue -dependent work or automatically re-probe in that transaction. Controllers await -their use cases so protocol failures propagate and cancelled polling completes -without leaving pending result futures. - -Key operations, certificate writes and management-key replacement in the controller -now use the upstream `Access::Existing` factories within the same reserved -transaction: signing (`piv_sign` with host-side digest/padding), X25519 derive, -ML-KEM-768 decapsulation, EC/RSA/Ed25519 key import, management-key replacement -and algorithm-configuration writes. A successful algorithm-configuration write -discards the prepared profile (upstream marks it `ReprobeRequired`). Certificate -imports go through `write_object`; libcanokey owns the 53 framing and PUT DATA -segmentation. The macOS setup serial reuses the probed Admin observation instead -of a raw Admin SELECT. Never infer access from UI PIN caches, or SELECT/probe between -authentication and its target. Host CSR construction and UI policy remain in Console. - -Streaming signing and attestation complete the facade: `piv_sign_streaming` -(mode 0 = ML-DSA-65 with the empty context, 1 = randomized Ed25519 wire mode FF, -2 = SM2 full message with an optional user ID) and `piv_attest` (INS F9) both use -`Access::Existing` inside the reserved transaction. ML-DSA-65 messages and Ed25519 -messages that are empty or exceed the classic 512-byte budget sign through the -streaming modes; classic digest signing (including the host-computed SM2 ZA -digest) stays with `piv_sign`. Streaming modes require evidenced 3.1.0 firmware -with observed enabled algorithm IDs, so pre-3.1 devices lose the 513–544-byte and -empty-message classic Ed25519 paths Console previously attempted. The controller -no longer builds GENERAL AUTHENTICATE or attestation APDUs. Post-quantum seed -import now uses the `pivImportPqSeed` binding with `Access::Existing` and a -prepared profile: the P1 algorithm ID comes from the probed profile, so an -unprobed PQ ID fails `CapabilityUnknown` before I/O, and the Default policy no -longer sends explicit AA/AB TLVs. `piv_post_quantum.dart`, `_sendChainedData`, -`_algorithmIdHex` and `PivCardClient.transceive` were removed; PIV contains no -hand-built APDUs. +# libcanokey integration + +All card protocol operations in Console go through +[libcanokey](https://github.com/canokeys/libcanokey) via the Flutter Rust +Bridge facade in `rust/src/api/protocol.rs`. The pinned revision lives in +`rust/Cargo.toml`. Dart never constructs APDUs; host-side policy (CSR +building, UI flows, credential prompting) stays in Console. + +## Architecture + +- `rust/src/api/protocol.rs` — the FRB facade. `ProtocolProfile` holds probed + device evidence; `ProtocolOperation` is an opaque state machine that Dart + drives APDU-by-APDU (`start()`/`advance(response)` → `ProtocolStep`). + Handles (`CtapPinSession`, `CtapPinToken`, profiles) are opaque and + close/dispose locally only. +- `lib/helper/utils/card_client.dart` — shared client infrastructure: + `CardClientBase` (transport/lease guards, `withSession`, cancellation), + `ProfileCardClient` (`prepareProfile`, `executePrepared`, + `lastStatusWord`), `ProfileBinding` (lease + generation evidence), and + `AdminSessionCardClient` (Admin progress and session evidence) in + `admin_card.dart`. Per-applet clients (`piv_card.dart`, `oath_card.dart`, + `openpgp_card.dart`, `ndef_card.dart`, `pass_card.dart`, + `webauthn_card.dart`) hold only domain logic. +- `lib/helper/utils/smartcard.dart` — the `SmartCard.process` queue; every + use case (connect, SELECT, authenticate, commands, cleanup) runs inside it. + Connection bootstrap identity commands use upstream `admin::command` + builders via `ProtocolOperation.bootstrapIdentity`. + +## Contracts that must not be broken + +- A physical lease (`CardLease`) owns selection/profile/authentication + generations. A closed, failed or replaced lease cannot exchange or publish + results. One executor reserves its lease for a whole operation; raw + exchanges, other operations and rebinds cannot interleave continuation + APDUs. +- Profiles are immutable evidence, never authentication tokens. Controllers + `prepare()` explicitly before dependent reads; an explicit SELECT or a + profile-affecting write invalidates the evidence. Failed re-probing never + leaves an older profile usable. +- Mutations are never retried, resumed or rolled back by the host. A + cancelled or uncertain write is exposed as unconfirmed, not reported + successful. +- Credentials are explicit per request and enter zeroizing containers; Dart + wipes its mutable copies. Admin session evidence (`Access::Existing` + reuse) is recorded only by a successful facade `VerifyPin`, is stamped + with the lease generations, and is invalidated by cross-applet selection, + profile invalidation, uncertain writes, failed Existing requests or lease + replacement. UI PIN caches never produce authorization. +- Capability policy: firmware outside the audited matrix is + `CapabilityUnknown` and unsupported operations fail at construction, + before any I/O. Console never bypasses capability checks with invented + profiles or speculative wire formats. +- Errors keep kind, phase and status word as separate fields. On CTAP paths + `statusWord` carries the raw CTAP status byte (widened), not an ISO SW. + Transport errors propagate unchanged. + +## Applet notes + +- **Admin** — full coverage (config, PIN, NFC, SM2, keymap, applet/factory + reset, Pass slots). Requests default to SELECT + per-request PIN and + converge to `Access::Existing` while session evidence is valid. +- **PIV** — full coverage, including PQ seed import, attestation and + streaming sign, all with `Access::Existing`. The algorithm-extension read + uses the upstream profile-based operation: on 3.0.x firmware it requires + management-key authentication (`Access::Management` authenticates and + reads in one operation); an unauthenticated read fails with + `SecurityStatusNotSatisfied` and the UI falls back to firmware defaults + only for that known gate. Certificate/object framing, gzip bounds and + continuation are owned upstream. +- **OATH** — full coverage; password-protected applets pass + `accessKey`/`accessChallenge` per operation. Set-default legacy dialects + are split by the upstream capability (`OathSetDefaultSlots`). +- **OpenPGP** — full coverage (data reads, PIN/reset-code/unblock, touch + policy/cache, retries, generate, terminate/activate). Optional data + objects map only `NotFound` to null. +- **NDEF** — profile-free read capability/message and crash-safe write. + The CC advertises the file ID; read-only and oversized writes fail at the + CC preflight (`SecurityStatusNotSatisfied` / `LimitExceeded`). +- **Pass** — Admin `PassSlots`/`SetPassSlot`. Both are protected; reads take + the lease's verified PIN unless session evidence applies. OATH-linked + slots are read-only here (configured via OATH); unknown slot types surface + as unknown. +- **WebAuthn/CTAP2** — upstream `ctap2`/`pin`/`credmgmt` client layer + (getInfo, ClientPin v1/v2, credential management) with canonical CBOR in + Rust. Tokens are ceremony-scoped and never cached; the ephemeral scalar + and IV are generated by the facade's CSPRNG. Non-zero CTAP status bytes + are data for the UI, not transport errors. ## Verification ```sh -flutter_rust_bridge_codegen generate -cargo test --manifest-path rust/Cargo.toml api::protocol --locked +flutter_rust_bridge_codegen generate # after facade signature changes +cargo test --manifest-path rust/Cargo.toml --locked cargo build --manifest-path rust/Cargo.toml --release --locked -flutter test --no-pub test/helper/utils/piv_card_test.dart test/helper/utils/webauthn_card_test.dart -flutter test --no-pub --exclude-tags native +cargo check --manifest-path rust/Cargo.toml --target wasm32-unknown-unknown --locked +flutter test --no-pub +flutter test --no-pub --tags native # injected-transport transcripts ``` -The native PIV transcript tests run alongside the WebAuthn CTAP2 tests in the -USB/IP workflow. -The firmware smoke entry point initializes FRB before the migrated PIV operations. -Native transcripts do not establish physical-device or legacy-firmware coverage. - -First-increment validation on 2026-09-15 (Rust 1.98.1 / Flutter 3.47.1): all 35 Rust tests, -292 non-native Dart/UI tests and 19 native PIV/FIDO2 tests passed. The generated -bridge built natively, `cargo check --target wasm32-unknown-unknown` and -`flutter build web --no-pub` passed. Changed Dart files passed analysis and the -workflow passed actionlint. Strict Clippy encountered the existing -`chunks_exact_to_as_chunks` lint in `rust/src/api/decode.rs:10`; all-target Clippy -passed with only that lint allowed. Repository-wide rustfmt also reports existing -formatting in `piv_crypto.rs`; the new protocol module passes rustfmt. No physical -card, browser transport session or USB/IP firmware matrix was run locally. - -Certificate/session increment validation on 2026-09-15: 37 Rust tests, 303 -non-native Dart/UI tests and 21 native PIV/FIDO2 tests passed. Coverage includes -exclusive use cases, expired callback leases, late responses after disconnect, -cancellation during I/O, poisoned transports, invalid/duplicate certificate TLVs, -gzip CRC/trailing-data failures and decompression limits. Changed Dart files passed -analysis, native release build and wasm32 check passed, and the FRB WASM package -and Flutter web application both built. The web helper initially hit the known -host-linker-flags failure; rebuilding with a temporary wasm-bindgen-cli 0.2.100 -installation resolved it without replacing the global tool. No physical-card, -browser transport or USB/IP firmware matrix was exercised for this increment. - -Profile/metadata increment validation on 2026-09-15: all 39 Rust tests and 306 -non-native Dart/UI tests passed. The final focused run passed 56 tests, including -28 native PIV/FIDO2 tests and session, executor, metadata model and controller -coverage. It verifies profile ownership and disposal, operation-wide exclusion, -failed re-probes, metadata after authentication without SELECT, legacy firmware -responses, malformed metadata and algorithm resolution against the current device -profile rather than cached UI configuration. Changed handwritten Dart files passed -analysis; the protocol module passed rustfmt and the diff passed whitespace checks. -The native release, wasm32 check, regenerated FRB WASM package and final Flutter web -application build passed. No physical-card, browser transport or USB/IP firmware -matrix was exercised for this increment. - -Credential/authentication increment validation on 2026-09-15: all 42 Rust tests, -300 non-native Dart/UI tests and 53 focused session/executor/native tests passed -(34 native PIV/FIDO2 tests). Six obsolete Dart authentication-builder tests were -removed with the unused implementation; native transcripts now exercise the -upstream authentication path, including TDES/AES known-answer vectors, legacy Le, -concurrent-command exclusion, malformed challenges, credential references/retries, -terminal mutation failures and profile invalidation. After updating controller -completion handling, its 81 controller/view tests also passed. Changed handwritten -Dart files passed analysis, the protocol module passed rustfmt, and the diff passed -whitespace checks. Native release, the regenerated FRB WASM package and Flutter web -application built successfully. No physical-card, browser transport or USB/IP -firmware matrix was exercised for this increment. - -Admin increment validation on 2026-09-15: all 44 Rust tests, 290 non-native Dart/UI -tests and 76 focused native/session/executor tests passed (57 native tests across -Admin, applet switches, PIV and FIDO2). The final selected-app publication checks -passed another 48 Admin/PIV tests. Coverage includes explicit per-request PINs, -legacy configuration and SM2 layouts, masked/no-op patches, retained partial-write -progress, cancellation/lost responses, no mutation replay, cross-applet selection -and profile invalidation, and cross-page credential-cache invalidation. Changed -handwritten Dart files passed analysis, the protocol module passed rustfmt and the -diff passed whitespace checks. Native release, regenerated FRB WASM and Flutter web -builds passed. The native workflow includes the new Admin/applet-switch tests; its -YAML parses successfully. No physical-card, browser transport or USB/IP firmware -matrix was exercised locally. - -## Existing-access increment and ownership boundary - -All libcanokey Cargo crates now pin `95e1930e` (including protocol and admin). The removed upstream selected-context -allocation has been replaced by `Access::Existing` or explicit `select=false`. -No profile is an authentication token: Console retains the physical lease, -selection/profile generation checks, operation ownership and cancellation cleanup. - -Console's executor records whether an exchange was attempted separately from the -upstream protocol error. This local fact governs whether failed prepared operations -may retain their lease evidence; protocol phase is not an I/O receipt. Expected -credential rejections and absent object reads retain their documented handling. - -Confirmed upstream issue: object/certificate reads at this revision can report -`InvalidResponse/Construction` for malformed outer containers returned by the -card. The missing `Parsing` annotation belongs to canokey-piv. A separate local -The pinned revision includes the PIV object parsing phase fix; Console performs -no protocol error remapping. Profile-free selected PIN status remains an upstream API gap in this revision; Admin existing-selection requests are available at the current pin and now carry Console's lease-evidenced Admin session reuse (see Admin integration). Keyboard layout/keymap reads and writes are now routed through upstream Admin operations. - -Validation for this increment: 44 Console Rust tests, 290 non-native Dart/UI tests, -82 focused native/session/executor tests and a final 46-test PIV/executor run passed. -Native release, regenerated FRB WASM and final Flutter Web builds passed; changed -handwritten Dart files passed analysis. The upstream regression failed before the -fix, then all 67 canokey-piv tests (including doctests) and strict workspace/all-target -clippy passed. Neither repository was tested with physical hardware or the USB/IP -firmware matrix in this increment. The libcanokey fix is committed locally only. - -OpenPGP increment validation on 2026-09-16: the 12 focused OpenPGP card tests -(native transcripts over injected transports) and the 12 OpenPGP page tests -passed, and changed Dart files passed analysis. The wider non-native suite -passed 260 tests with 5 pre-existing OATH compile-failure suites untouched by -this increment (`oath_card.dart` belonged to the parallel OATH migration). The -host needed an `xcrun` shim exporting `DEVELOPER_DIR` for the Command Line -Tools SDK because the Xcode license prompt breaks the objective_c native-asset -hook. - -OpenPGP administrative-writes increment validation on 2026-09-16: the bridge -gained `openpgp_write_reset_code` (Option clears), `openpgp_reset_retries`, -`openpgp_write_signature_pin_policy`, `openpgp_unblock_with_admin`, -`openpgp_unblock_with_code`, `openpgp_write_touch_policy` and -`openpgp_write_touch_cache_time`; all seven Console operations moved off raw -APDUs, and the client's manual SELECT/PUT DATA/RESET RETRY COUNTER builders -were removed. All 53 Rust tests passed, including new wire-byte, argument -validation, capability-gate (UIF from 1.5.2, retry reset from 3.1.0) and -credential-reference/reset-code error-mapping coverage. After FRB -regeneration, the 14 native OpenPGP card tests plus focused PIV/Admin native -suites and the 12 OpenPGP page tests passed (81 tests), changed handwritten -Dart files passed analysis, and `cargo build --release --locked` passed. -Reset-code clearing is a dataless PUT DATA D3 (no Lc) on the wire, matching -upstream framing. The USB/IP smoke harness keeps its session/prepare flow -but was not run locally; no physical card, browser transport or USB/IP -firmware matrix was exercised. - -OATH increment validation on 2026-09-16: all 53 Rust tests passed, including -new protocol-module coverage for per-operation access validation, protected -applet rejection before the target command, the name/marker-preserving -calculation encoding and the legacy 1.3 dialect gate. The 9 native OATH card -tests (injected-transport transcripts with canokey-oath known-answer HMACs), -the OATH model/view tests and the full non-native suite (276 tests) passed, as -did the focused native PIV/Admin/session/executor suites (74 tests) after the -FRB regeneration. Changed Dart files passed analysis; `cargo build --release ---locked` passed. FRB WASM regeneration and the Flutter web build were not run -locally, and no physical card, browser transport or USB/IP firmware matrix was -exercised. - -PQ-import/OATH-set-default/NDEF/CTAP/Pass increment validation on 2026-09-16: -the pin moved to `95e1930e`, which brings the new canokey-ctap and canokey-ndef -crates plus upstream PIV PQ seed import, OATH SetDefault, Admin -PassSlots/SetPassSlot and `admin::Access::Existing`/`operation_with_access`. -`cargo test --locked` passed in full (65 tests, including 9 new facade-binding -tests). New native-transcript Dart tests: 4 PIV PQ, 6 OATH set-default, 10 -NDEF, 11 CTAP and 8 Pass. The full `flutter test` suite passed (376 tests). -The FRB WASM package was regenerated and `flutter build web --no-pub` passed -(host-side wasm-bindgen-cli 0.2.100 pre-installed to dodge the known -host-linker-flags leak from the wasm RUSTFLAGS). The USB/IP firmware -matrix and physical-card runs were not exercised for this increment. - -Admin existing-access increment validation on 2026-09-16: the `existing` -parameter now reaches `adminRead`/`adminConfigure`/`adminAction`, and Console -records lease-bound Admin session evidence only on an explicit facade -`VerifyPin`, stamped with the lease's selection/profile generations. -Protected requests reuse `Access::Existing` while that evidence is valid and -fall back to `existing: false` plus an explicit per-request PIN after any -applet selection, profile invalidation, exposed/uncertain mutation, failed -Existing request or lease replacement. `cargo test --locked` passed in full -(66 tests, including existing-access wire coverage for the three general -Admin entries). 124 native-tagged Dart tests passed, including new -transcripts asserting that Existing requests send no SELECT/VERIFY, that a -confirmed mutation, a cross-applet selection (PIV probe) and a failed -Existing request each end the evidence, and that the Pass client's own probe -on a shared lease ends another client's recorded session. The non-native -suite passed (257 tests, including the lease-evidence generation unit tests -and the settings configuration tests against the `prepareIfStale` refresh). -Changed Dart files passed analysis and the protocol module passes rustfmt. -After this increment the FRB WASM package was regenerated again and -`flutter build web --no-pub` passed, strict clippy is warning-free and the -full `flutter test` run passed (381 tests). No physical card, browser -transport or USB/IP firmware matrix was exercised for this increment. - -CTAP2 client-layer increment validation on 2026-09-16: `cargo test --locked` -passed in full (74 tests, including 8 new facade tests covering the getInfo -field encoding and tri-states, golden ClientPIN wire bytes for V1/V2 against -the upstream known answers, fresh-IV generation, pinUvAuthToken handles, -RP/credential enumeration encodings including the metadataOnly extension, -deleteCredential, and raw-CTAP-status error mapping). FRB 2.13.0 regeneration, -`cargo build --release --locked` and `cargo check --target -wasm32-unknown-unknown --locked` passed (`rand` moved to `[dependencies]`; -wasm entropy still comes from getrandom's wasm_js feature). Dart business -code is untouched; the controller migration is a follow-up increment. - -## Card-client boilerplate consolidation (2026-09-16) - -A pure-Dart, behavior-preserving refactoring after the applet migrations: the -six card clients now share `CardClientBase` (transport/lease guard, -`withSession`, cancellation) and `ProfileCardClient` (prepared-profile -ownership, generic `prepareProfile` and `executePrepared` with per-applet -flags for applet pre-selection, profile-identity checks and profile-discard -policy) in `lib/helper/utils/card_client.dart`, plus one `ProfileBinding` -class (lease + profile generation, optionally the selection generation for -PIV) replacing the five per-applet binding classes. Admin and Pass share -`AdminSessionCardClient` (in admin_card.dart) for the lease-evidenced -`Access::Existing` request path and Admin progress handling; NDEF stays -profile-free on `CardClientBase`. Admin/Pass `lastResponse` was renamed to -`lastStatusWord` for uniformity (the three controller call sites updated). -No protocol flow, lease/profile/selection generation check, error mapping or -public API semantics changed; the existing card-client, session, executor and -native transcript suites pin the behavior. `dart analyze` reports no issues -in the changed files, and the full `flutter test --no-pub` (383 tests) and -`flutter test --no-pub --tags native` (126 tests) runs passed. - -## WebAuthn CTAP2 client-layer migration (2026-09-16) - -The WebAuthn controller migrated off the Dart fido2 package onto the facade's -CTAP2 client layer. `lib/helper/utils/webauthn_card.dart` (a `CardClientBase`) -parses the getInfo/RP/credential byte encodings, owns the PIN session/token -handle lifecycles (fresh `beginPinSession` + `getPinTokenWithPermissions` per -use case, pinUvAuthProtocol V2 preferred when advertised) and marks every -operation as applet-selecting; `protocol_operation.dart` gained PIN -session/token executors. `_showPinError` recovers the PIN_INVALID (0x31) / -PIN_AUTH_BLOCKED (0x34) / PIN_BLOCKED (0x32) / PIN_POLICY_VIOLATION (0x37) -distinction from the raw CTAP status byte in `ProtocolError.statusWord`. -`WebAuthnItem.credentialId` is a local `Uint8List`. Removed: the fido2 2.0.0 -pub dependency, `fido2_backend*.dart`, `ctap_transmitter.dart`, the -`initializeFido2Backend` startup calls, the `web/fido2` WASM artifacts and -their `index.html` loader, the `fido2_crypto` crate and its retained C ABI in -`rust/src/lib.rs`, and the FIDO2 web backend steps in deploy.yml (the usbip -workflow's backend step no longer exports `FIDO2_CRYPTO_LIBRARY`). The -wasm-bindgen family pins stay, with the comment reworded; FRB bindings were -not regenerated (no facade change). - -Validation: `cargo test --locked` (74 tests), `cargo build --release --locked` -and `cargo check --target wasm32-unknown-unknown --locked` passed; -THIRD_PARTY_LICENSES.json regenerated without fido2_crypto. The new -`test/helper/utils/webauthn_card_test.dart` replays the upstream canokey-ctap -golden transcripts over injected transports (getInfo tri-states/minPinLength/ -protocols, V1/V2 key agreement, set/changePIN, token minting, RP/credential -enumeration encodings, deletion, empty-enumeration, closed-handle and -status-byte error mapping); the old fido2_backend/ctap_transmitter tests were -removed. `flutter test --no-pub` passed (387 tests, including the updated -WebAuthn page/model tests and the usbip smoke's new `webauthn.client.get_info` -section), `flutter test --no-pub --tags native` passed (130 tests), and -`dart analyze` reports no issues. `flutter build web --no-pub` passed with no -fido2 reference left in the web output. The FRB WASM package was not -regenerated (no Rust facade change); no physical card, browser transport or -USB/IP firmware matrix was exercised. - -## Firmware authentication-gate modeling (2026-09-16, rev 9110bc7) - -The pin moved to `9110bc71a0b00f3517b96651ed63e030f8fffadc`, which models the -firmware authentication gates on reads upstream: Admin INS 43/44 (PASS) reads -are unconditionally protected, so `Access::None` fails at construction with -SecurityStatusNotSatisfied and zero I/O; the new -`Capability::PivProtectedAlgorithmConfigRead` (3.0.0–3.0.3) makes -`piv::read_algorithm_config` reject `Access::None`/`Access::Pin` at -construction while allowing `Existing`/`Management`. The facade gained the -profile-based `pivReadAlgorithmConfig` binding: without a key it uses -`Access::Existing`; with a key it uses -`Access::Management(ManagementAuthentication::external(...))`, and upstream -SELECTs, authenticates (GENERAL AUTHENTICATE) and reads within one operation. -The capability check runs at construction before any authentication, so -firmware without the INS EE read never receives a GENERAL AUTHENTICATE. The -Dart client's manual select→prepare→authenticate→read orchestration was -removed; `PivReadOperation.algorithmConfiguration` (profile-free -`read_configuration_selected`) was removed from the bridge, and the -controller's `>= 3.0.0 && < 3.1.0` fallback gate was replaced by an -unconditional SecurityStatusNotSatisfied fallback. CTAP failures now arrive in -upstream `Error::application_status` (status_word is ISO-only again); the -bridge widens the byte into `ProtocolError.statusWord`, so the Dart contract -is unchanged. The facade's `canokey` dependency enables the now-opt-in -`clientpin` feature. The OATH `Outcome::Serial`/`ChallengeResponse` variants -(the renamed vendor challenge-response API) are not exposed by the facade and -map to InvalidResponse if ever produced. - -Validation: `cargo test --locked` passed in full (78 tests, including new -facade coverage for the application_status mapping, the capability-gate -precedes-authentication ordering on 2.0.0, the 3.0.3 SELECT+GA+read transcript -against the known TDES answer, the unauthenticated 6982 path and the PassSlots -bare-read construction gate). FRB 2.13.0 regeneration, `cargo build --release ---locked`, `cargo check --target wasm32-unknown-unknown --locked`, -all-targets clippy and rustfmt passed. `flutter test --no-pub` passed (388 -tests), `flutter test --no-pub --tags native` passed (131 tests, including the -rewritten Pass transcripts: bare read fails at construction with zero I/O, -PIN reads keep their SELECT+VERIFY+read transcripts), and `dart analyze` -reports no errors. The usbip smoke passes the default management key -unconditionally for the extension read. No physical card, browser transport -or USB/IP firmware matrix was exercised for this increment. +The FRB WASM package (`flutter_rust_bridge_codegen build-web --release +--wasm-pack-rustup-toolchain `) and `flutter build web --no-pub` +must be rebuilt after facade changes; the wasm-bindgen crate family in +`rust/Cargo.toml` tracks the `wasm-bindgen-cli` version used by wasm-pack. +The USB/IP firmware matrix runs in CI (`.github/workflows/usbip.yml`) via +`test/usbip/console_smoke.dart`; physical-card and browser-transport +coverage is not exercised locally. diff --git a/lib/helper/utils/ndef_card.dart b/lib/helper/utils/ndef_card.dart index 2294543..456426e 100644 --- a/lib/helper/utils/ndef_card.dart +++ b/lib/helper/utils/ndef_card.dart @@ -21,9 +21,11 @@ class NdefReadOnlyException implements Exception { } /// libcanokey-backed NDEF operations. These profile-free operations SELECT the -/// NDEF applet and own CC/file selection, 240-byte chunking, continuation and -/// the crash-safe write order (zero NLEN, message, real NLEN). A failed write -/// is never replayed; transport errors propagate unchanged. +/// NDEF applet and own CC/file selection (the file ID is CC-advertised), +/// 240-byte chunking, continuation and the crash-safe write order (zero NLEN, +/// message, real NLEN), preflighting read-only and oversized writes against +/// the CC. A failed write is never replayed; transport errors propagate +/// unchanged. class NdefCardClient extends CardClientBase { NdefCardClient({super.transport, super.lease}); @@ -64,7 +66,10 @@ class NdefCardClient extends CardClientBase { error.details.phase == 'Select') { return null; } - if (error.details.statusWord == 0x6982) { + // A read-only file is rejected at the CC preflight (no status word) or, + // on older library behavior, by the card's 6982 at UPDATE. + if (error.details.kind == 'SecurityStatusNotSatisfied' || + error.details.statusWord == 0x6982) { throw const NdefReadOnlyException(); } rethrow; diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ed24571..9b7176a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,7 +355,7 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-admin", "canokey-compat", @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-compat", "canokey-protocol", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-protocol", ] @@ -387,7 +387,7 @@ dependencies = [ [[package]] name = "canokey-ctap" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "aes 0.8.4", "canokey-protocol", @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-compat", "canokey-protocol", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "canokey-ndef" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-protocol", ] @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-compat", "canokey-protocol", @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "canokey-compat", "canokey-key", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=9110bc71a0b00f3517b96651ed63e030f8fffadc#9110bc71a0b00f3517b96651ed63e030f8fffadc" +source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" dependencies = [ "thiserror 2.0.20", "zeroize", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 94ae293..548c6ca 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "9110bc71a0b00f3517b96651ed63e030f8fffadc", features = ["clientpin"] } -canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "9110bc71a0b00f3517b96651ed63e030f8fffadc" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "41a3ea6059a58eb7a546e4a74bee891be5602bc0", features = ["clientpin"] } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "41a3ea6059a58eb7a546e4a74bee891be5602bc0" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs index 4034c36..cff03e6 100644 --- a/rust/src/api/protocol.rs +++ b/rust/src/api/protocol.rs @@ -4255,14 +4255,15 @@ mod tests { let error = op.advance(malformed_cc).error.unwrap(); assert_eq!(error.kind, "InvalidResponse"); - // Message read: NLEN then the message from offset two; NLEN zero ends. + // Message read: the file ID comes from the CC (0xE104), then NLEN + // and the message from offset two; NLEN zero ends. let mut op = ProtocolOperation::ndef_read_message(); op.start(); op.advance(vec![0x90, 0]); op.advance(vec![0x90, 0]); assert_eq!( op.advance(cc(0)).command.unwrap(), - [0, 0xa4, 0, 0x0c, 2, 0, 1] + [0, 0xa4, 0, 0x0c, 2, 0xe1, 0x04] ); assert_eq!( op.advance(vec![0x90, 0]).command.unwrap(), @@ -4277,14 +4278,45 @@ mod tests { #[test] fn ndef_write_is_crash_safe_three_phase_and_bounded() { + let cc = |write_access: u8| { + let mut cc = vec![ + 0x00, + 0x0f, + 0x20, + 0x00, + 0xff, + 0x00, + 0xff, + 0x04, + 0x06, + 0xe1, + 0x04, + 0x04, + 0x00, + 0x00, + write_access, + ]; + cc.extend([0x90, 0]); + cc + }; let mut op = ProtocolOperation::ndef_write_message(b"hi".to_vec()); assert_eq!( op.start().command.unwrap(), [0, 0xa4, 4, 0, 7, 0xd2, 0x76, 0, 0, 0x85, 1, 1] ); + // The CC is read before any UPDATE: it advertises the file ID (0xE104) + // and the write access. assert_eq!( op.advance(vec![0x90, 0]).command.unwrap(), - [0, 0xa4, 0, 0x0c, 2, 0, 1] + [0, 0xa4, 0, 0x0c, 2, 0xe1, 0x03] + ); + assert_eq!( + op.advance(vec![0x90, 0]).command.unwrap(), + [0, 0xb0, 0, 0, 15] + ); + assert_eq!( + op.advance(cc(0)).command.unwrap(), + [0, 0xa4, 0, 0x0c, 2, 0xe1, 0x04] ); // Zero NLEN first: an interrupted write leaves no stale message. assert_eq!( @@ -4301,12 +4333,12 @@ mod tests { ); assert_eq!(op.advance(vec![0x90, 0]).data.unwrap(), Vec::::new()); - // A read-only file is the card's 6982, never a host-side guess. + // A read-only CC fails the write at preflight, before any UPDATE. let mut op = ProtocolOperation::ndef_write_message(b"hi".to_vec()); op.start(); op.advance(vec![0x90, 0]); op.advance(vec![0x90, 0]); - let error = op.advance(vec![0x69, 0x82]).error.unwrap(); + let error = op.advance(cc(1)).error.unwrap(); assert_eq!(error.kind, "SecurityStatusNotSatisfied"); // Messages beyond the firmware maximum fail before any I/O. diff --git a/test/helper/utils/ndef_card_test.dart b/test/helper/utils/ndef_card_test.dart index f1cc5de..087377c 100644 --- a/test/helper/utils/ndef_card_test.dart +++ b/test/helper/utils/ndef_card_test.dart @@ -12,7 +12,7 @@ import 'package:flutter_test/flutter_test.dart'; const _selectApplet = '00A4040007D2760000850101'; const _selectCc = '00A4000C02E103'; -const _selectNdef = '00A4000C020001'; +const _selectNdef = '00A4000C02E104'; const _readCc = '00B000000F'; const _readNlen = '00B0000002'; @@ -71,13 +71,19 @@ void main() { }); test('writes zero NLEN, 240-byte chunks and the real NLEN last', () async { - final transport = _QueueApduTransport(List.filled(6, '9000')); + final cc = _capabilityContainer(1024); + final transport = _QueueApduTransport([ + '9000', '9000', _ok(cc), // capability preflight + '9000', '9000', '9000', '9000', '9000', + ]); final message = Uint8List.fromList(List.generate(241, (index) => index)); await _withClient(transport, (client) async { expect(await client.write(message), isTrue); expect(transport.commands, [ _selectApplet, + _selectCc, + _readCc, _selectNdef, '00D60000020000', '00D60002F0${hex.encode(message.sublist(0, 240)).toUpperCase()}', @@ -88,11 +94,17 @@ void main() { }); test('writes an empty message as two NLEN updates', () async { - final transport = _QueueApduTransport(List.filled(4, '9000')); + final cc = _capabilityContainer(1024); + final transport = _QueueApduTransport([ + '9000', '9000', _ok(cc), + '9000', '9000', '9000', + ]); await _withClient(transport, (client) async { expect(await client.write(Uint8List(0)), isTrue); expect(transport.commands, [ _selectApplet, + _selectCc, + _readCc, _selectNdef, '00D60000020000', '00D60000020000', @@ -109,19 +121,16 @@ void main() { }); }); - test('maps a 6982 write rejection to NdefReadOnlyException', () async { - final transport = _QueueApduTransport(['9000', '9000', '6982']); + test('maps a read-only capability container to NdefReadOnlyException', () async { + final cc = _capabilityContainer(1024, readOnly: true); + final transport = _QueueApduTransport(['9000', '9000', _ok(cc)]); await _withClient(transport, (client) async { await expectLater( client.write(Uint8List.fromList([1])), throwsA(isA()), ); - // The failed write stopped at the zero-NLEN update; nothing is replayed. - expect(transport.commands, [ - _selectApplet, - _selectNdef, - '00D60000020000', - ]); + // The preflight stops after the CC read; no UPDATE is ever sent. + expect(transport.commands, [_selectApplet, _selectCc, _readCc]); }); }); From fd4ebdc019e9acb78094e99742d3a17af8730e66 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 19:53:57 +0800 Subject: [PATCH 09/19] chore: drop the dead resetApdu enum field --- lib/models/canokey.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/models/canokey.dart b/lib/models/canokey.dart index 8227a26..ebd5b00 100644 --- a/lib/models/canokey.dart +++ b/lib/models/canokey.dart @@ -4,15 +4,14 @@ import 'package:canokey_console/models/keyboard_keymap.dart'; final log = Logging.logger('CanoKey:Model'); enum Applet { - openpgp(resetApdu: '00030000', name: 'OpenPGP'), - piv(resetApdu: '00040000', name: 'PIV'), - webauthn(resetApdu: '00090000', name: 'WebAuthn'), - oath(resetApdu: '00050000', name: 'OTP'), - ndef(resetApdu: '00070000', name: 'NFC Tag'), - pass(resetApdu: '00130000', name: 'Pass'); + openpgp(name: 'OpenPGP'), + piv(name: 'PIV'), + webauthn(name: 'WebAuthn'), + oath(name: 'OTP'), + ndef(name: 'NFC Tag'), + pass(name: 'Pass'); - const Applet({required this.resetApdu, required this.name}); - final String resetApdu; + const Applet({required this.name}); final String name; } From 5e32c7dd8a27bb146e0b0213cc1b58f12b84af1c Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 20:35:07 +0800 Subject: [PATCH 10/19] fix: dart2js-safe 64-bit decodes and skip redundant Admin SELECTs ByteData getUint64/getInt64 are unsupported by dart2js, which broke the WebAuthn page on web; decode both halves with 32-bit accessors instead. PIN-less Admin requests now reuse the probe's selection while it is still current (Access::Existing), dropping a redundant SELECT per read; the Pass slot read stays construction-gated because the card gates it on every audited firmware. --- docs/libcanokey-migration.md | 5 +++- lib/helper/utils/admin_card.dart | 14 +++++++-- lib/helper/utils/card_client.dart | 12 +++++++- lib/helper/utils/pass_card.dart | 4 +++ lib/helper/utils/webauthn_card.dart | 18 +++++++++-- test/helper/utils/admin_card_test.dart | 33 +++++++++++---------- test/helper/utils/applet_switches_test.dart | 2 -- 7 files changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 94277b3..70a940e 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -58,7 +58,10 @@ building, UI flows, credential prompting) stays in Console. - **Admin** — full coverage (config, PIN, NFC, SM2, keymap, applet/factory reset, Pass slots). Requests default to SELECT + per-request PIN and - converge to `Access::Existing` while session evidence is valid. + converge to `Access::Existing` while session evidence is valid. A PIN-less + request also reuses the probe's selection (`Access::Existing`, no SELECT) + while it is still current, except for the card-gated Pass slot read; the + card remains the enforcement point either way. - **PIV** — full coverage, including PQ seed import, attestation and streaming sign, all with `Access::Existing`. The algorithm-extension read uses the upstream profile-based operation: on 3.0.x firmware it requires diff --git a/lib/helper/utils/admin_card.dart b/lib/helper/utils/admin_card.dart index 1c8f4b3..ba5a631 100644 --- a/lib/helper/utils/admin_card.dart +++ b/lib/helper/utils/admin_card.dart @@ -30,19 +30,27 @@ abstract class AdminSessionCardClient extends ProfileCardClient { /// Runs one Admin request. When this lease holds a still-valid recorded /// Admin authentication (see CardLease.recordAdminAuthentication) and the /// caller allows it, the request uses `Access::Existing`: no SELECT and no - /// VERIFY are sent, and any supplied PIN stays unused. Otherwise the - /// request SELECTs and verifies only the explicitly supplied PIN. + /// VERIFY are sent, and any supplied PIN stays unused. A PIN-less request + /// also uses `Access::Existing` while the probe's Admin selection is still + /// current, skipping a redundant SELECT — unless [selectionSufficient] is + /// false for requests whose target is card-gated regardless of firmware + /// (Pass slots); there the card remains the enforcement point either way. + /// Otherwise the request SELECTs and verifies only the explicitly supplied + /// PIN. Future executeAdminRequest( ProtocolOperation Function(ProtocolProfile profile, Uint8List? pin, bool existing) create, { String? pin, bool existingAllowed = true, + bool selectionSufficient = true, bool mutation = false, }) async { final binding = preparedBinding; lastStatusWord = null; lastProgress = null; - final existing = existingAllowed && binding.lease.hasAdminAuthentication; + final existing = existingAllowed && + (binding.lease.hasAdminAuthentication || + (selectionSufficient && pin == null && binding.selectionFresh)); if (!existing) { // Selecting factories invalidate any selected PIV context before // starting, even though immutable Admin profile evidence can be reused. diff --git a/lib/helper/utils/card_client.dart b/lib/helper/utils/card_client.dart index 4e672c9..1ff668e 100644 --- a/lib/helper/utils/card_client.dart +++ b/lib/helper/utils/card_client.dart @@ -17,14 +17,24 @@ String? formatStatusWord(int? status) => class ProfileBinding { ProfileBinding(this.profile, this.lease, {bool bindSelection = false}) : profileGeneration = lease.profileGeneration, - selectionGeneration = bindSelection ? lease.selectionGeneration : null; + selectionGeneration = bindSelection ? lease.selectionGeneration : null, + _observedSelectionGeneration = lease.selectionGeneration; final ProtocolProfile profile; final CardLease lease; final int profileGeneration; final int? selectionGeneration; + final int _observedSelectionGeneration; bool _closed = false; + /// True while no applet selection has happened since this binding's probe + /// left its applet selected. Non-fatal evidence: callers that can re-SELECT + /// fall back instead of failing. + bool get selectionFresh => + !_closed && + profileGeneration == lease.profileGeneration && + _observedSelectionGeneration == lease.selectionGeneration; + void check() { lease.check(); final selection = selectionGeneration; diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index 34e7902..cda800a 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -27,6 +27,10 @@ class PassCardClient extends AdminSessionCardClient { (profile, pinBytes, existing) => profile.adminPassSlots(pin: pinBytes, existing: existing), pin: pin, + // The slot read is card-gated on every audited firmware; a fresh + // selection alone must not turn the construction-time rejection into + // an on-card 6982. + selectionSufficient: false, ); return PassSlot.decode(result.data); } diff --git a/lib/helper/utils/webauthn_card.dart b/lib/helper/utils/webauthn_card.dart index 9e461d5..cdf5b05 100644 --- a/lib/helper/utils/webauthn_card.dart +++ b/lib/helper/utils/webauthn_card.dart @@ -54,7 +54,7 @@ class WebAuthnInfo { if (data.length - offset < 8) { throw const FormatException('Truncated WebAuthn minPinLength'); } - minPinLength = ByteData.sublistView(data).getUint64(offset); + minPinLength = _readUint64(data, offset); offset += 8; } else if (minPinLengthFlag != 0) { throw const FormatException('Invalid WebAuthn minPinLength flag'); @@ -73,6 +73,18 @@ class WebAuthnInfo { } } +// dart2js has no 64-bit ByteData accessors; combine two 32-bit halves instead. +// Exact for all values below 2^53, which covers both fields by construction. +int _readUint64(Uint8List data, int offset) { + final view = ByteData.sublistView(data); + return view.getUint32(offset) * 0x100000000 + view.getUint32(offset + 4); +} + +int _readInt64(Uint8List data, int offset) { + final view = ByteData.sublistView(data); + return view.getInt32(offset) * 0x100000000 + view.getUint32(offset + 4); +} + /// One relying party with resident credentials. class WebAuthnRp { const WebAuthnRp({required this.id, required this.name, required this.idHash}); @@ -285,14 +297,14 @@ class WebAuthnPinToken { if (data.length - offset < 8) { throw const FormatException('Truncated WebAuthn COSE algorithm'); } - coseAlgorithm = ByteData.sublistView(data).getInt64(offset); + coseAlgorithm = _readInt64(data, offset); offset += 8; publicKey = null; case 0x02: if (data.length - offset < 8) { throw const FormatException('Truncated WebAuthn COSE algorithm'); } - coseAlgorithm = ByteData.sublistView(data).getInt64(offset); + coseAlgorithm = _readInt64(data, offset); offset += 8; publicKey = takeBytes((take() << 8) | take(), 'public key'); default: diff --git a/test/helper/utils/admin_card_test.dart b/test/helper/utils/admin_card_test.dart index 512dab2..ad3ee48 100644 --- a/test/helper/utils/admin_card_test.dart +++ b/test/helper/utils/admin_card_test.dart @@ -37,13 +37,9 @@ void main() { ..[1] = 0x81 ..[5] = 42; final transport = _Transport([ - '9000', '01000001013F9000', - '9000', '02089000', - '9000', '${hex.encode(usage)}9000', - '9000', 'AABB9000', ]); await _prepared(transport, (client) async { @@ -52,14 +48,11 @@ void main() { expect((flash.usedKiB, flash.totalKiB), (2, 8)); expect(await client.readAppletStorageUsage(), usage); expect(await client.readChipId(), 'AABB'); + // The probe left Admin selected, so each read skips its own SELECT. expect(transport.commands, [ - _select, '0042000000', - _select, '0041000000', - _select, '0041010000', - _select, '0032010000', ]); }); @@ -67,14 +60,21 @@ void main() { ); test( - 'unverified protected reads still require an explicit per-request PIN', + 'unverified protected reads are rejected and an explicit per-request PIN works', () async { - final transport = _Transport(['9000', '9000', '0100010101019000']); + // Firmware 2.0.0 gates the configuration read on-card: the PIN-less + // read reuses the fresh selection and the card itself rejects it. + final transport = _Transport(['6982', '9000', '9000', '0100010101019000']); await _prepared(transport, (client) async { await expectLater(client.readConfig(), _kind('SecurityStatusNotSatisfied')); - expect(transport.commands, isEmpty); + expect(transport.commands, ['0042000000']); expect(await client.readConfig(pin: '123456'), [1, 0, 1, 1, 1, 1]); - expect(transport.commands, ['${_select}00', '${_verify}00', '0042000000']); + expect(transport.commands, [ + '0042000000', + '${_select}00', + '${_verify}00', + '0042000000', + ]); }, firmware: '2.0.0'); }, ); @@ -481,14 +481,14 @@ void main() { test('optional commit only falls back on unsupported feature', () async { for (final response in ['6D00', '9000', '6162639000']) { - await _prepared(_Transport(['9000', response]), (client) async { + await _prepared(_Transport([response]), (client) async { expect( await client.readCoreCommit(), response == '6162639000' ? 'abc' : null, ); }); } - await _prepared(_Transport(['9000', '6982']), (client) async { + await _prepared(_Transport(['6982']), (client) async { await expectLater( client.readCoreCommit(), _kind('SecurityStatusNotSatisfied'), @@ -503,13 +503,14 @@ void main() { await client.resetNdef(pin: '123456'); expect(client.lastProgress!.confirmedWrites, 1); }); - final transport = _Transport(['9000', '6985']); + final transport = _Transport(['6985']); await _prepared(transport, (client) async { await expectLater( client.factoryReset(), _kind('ConditionsNotSatisfied'), ); - expect(transport.commands, [_select, '00500000055245534554']); + // Factory reset submits no PIN; a fresh selection skips the SELECT. + expect(transport.commands, ['00500000055245534554']); expect(client.lastProgress!.reprobeRequired, isTrue); }); }, diff --git a/test/helper/utils/applet_switches_test.dart b/test/helper/utils/applet_switches_test.dart index 4c643fa..585bb26 100644 --- a/test/helper/utils/applet_switches_test.dart +++ b/test/helper/utils/applet_switches_test.dart @@ -49,7 +49,6 @@ void main() { '${hex.encode(utf8.encode('3.1.0'))}9000', '43616e6f4b65799000', '010203049000', - '9000', '01000001000d9000', ]); final client = AdminCardClient(transport: transport); @@ -61,7 +60,6 @@ void main() { '0031000000', '0031010000', '0032000000', - '00A4040005F000000000', '0042000000', ]); expect(status.featureSwitchesSupported, isTrue); From 0b235854c2a7cd36d8804047a98f269b24c3c2d9 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 21:05:13 +0800 Subject: [PATCH 11/19] feat: show build commit and build time in the About dialog Both are injected at compile time via BUILD_COMMIT/BUILD_TIME dart-defines; CI workflows and the fastlane release lanes inject them automatically, local development builds simply omit the line. --- .github/workflows/build.yml | 4 +++- .github/workflows/deploy.yml | 2 +- android/fastlane/Fastfile | 9 ++++++++- docs/release.md | 11 +++++++++-- ios/fastlane/Fastfile | 9 ++++++++- lib/helper/utils/build_info.dart | 13 +++++++++++++ .../settings/widgets/other_settings_card.dart | 11 +++++++++++ 7 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 lib/helper/utils/build_info.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acdce95..bec0ac6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,9 @@ jobs: run: cargo install flutter_rust_bridge_codegen --version 2.13.0 --locked - name: Run flutter release build (${{ matrix.target }}) run: | - flutter build ${{ matrix.target }} --release ${{ matrix.build-args }} --verbose + flutter build ${{ matrix.target }} --release ${{ matrix.build-args }} --verbose \ + --dart-define=BUILD_COMMIT="$GITHUB_SHA" \ + --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" if [ "${{ matrix.target }}" == "linux" ]; then sudo sh -c 'umask 022; exec /snap/bin/snapcraft pack --destructive-mode --platform "$1"' _ "${{ matrix.arch }}" sudo chown "$USER:$USER" ./*.snap diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 05b36f4..5810033 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,7 +49,7 @@ jobs: cargo install wasm-pack --version "$WASM_PACK_VERSION" --locked flutter_rust_bridge_codegen build-web --release \ --wasm-pack-rustup-toolchain "$RUST_NIGHTLY" - - run: flutter build web --verbose + - run: flutter build web --verbose --dart-define=BUILD_COMMIT="$GITHUB_SHA" --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - uses: actions/upload-artifact@v7 with: name: web-spa diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index db9517b..e89487e 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -7,11 +7,18 @@ AAB_BUILD_PATH = File.join(ROOT_DIR, "build", "app", "outputs", "bundle", "relea AAB_DIST_PATH = File.join(ROOT_DIR, "dist", "android", "CanoKey-Console.aab") ANDROID_METADATA_DIR = File.join(__dir__, "metadata", "android") +# Compile-time build metadata shown in the About dialog. +def build_defines + commit = ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + ["--dart-define=BUILD_COMMIT=#{commit}", "--dart-define=BUILD_TIME=#{time}"] +end + platform :android do desc "Build the signed release AAB without uploading it" lane :build_internal do Dir.chdir(ROOT_DIR) do - sh("flutter", "build", "appbundle", "--release") + sh("flutter", "build", "appbundle", "--release", *build_defines) end UI.user_error!("AAB was not generated at #{AAB_BUILD_PATH}") unless File.exist?(AAB_BUILD_PATH) diff --git a/docs/release.md b/docs/release.md index 55f4197..80cb15c 100644 --- a/docs/release.md +++ b/docs/release.md @@ -25,10 +25,17 @@ RUSTUP_TOOLCHAIN=1.98.1 flutter_rust_bridge_codegen generate rustup run 1.98.1 cargo test --manifest-path rust/Cargo.toml flutter test dart analyze -flutter build ios --release --no-codesign -flutter build appbundle --release +flutter build ios --release --no-codesign \ + --dart-define=BUILD_COMMIT="$(git rev-parse HEAD)" \ + --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +flutter build appbundle --release \ + --dart-define=BUILD_COMMIT="$(git rev-parse HEAD)" \ + --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" ``` +The `BUILD_COMMIT`/`BUILD_TIME` dart-defines are shown in the About dialog; +CI and the fastlane lanes inject them automatically. + Test NFC and CCID workflows on physical iOS and Android devices. On Android, also verify that each selectable NFC sound plays and that disabling the setting silences it. diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index e440228..0ae9aae 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -6,13 +6,20 @@ ROOT_DIR = File.expand_path("../..", __dir__) IPA_PATH = File.join(ROOT_DIR, "dist", "ios", "CanoKey-Console.ipa") IOS_METADATA_DIR = File.join(__dir__, "metadata") +# Compile-time build metadata shown in the About dialog. +def build_defines + commit = ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + ["--dart-define=BUILD_COMMIT=#{commit}", "--dart-define=BUILD_TIME=#{time}"] +end + platform :ios do desc "Build the signed App Store IPA without uploading it" lane :build_testflight do FileUtils.mkdir_p(File.dirname(IPA_PATH)) Dir.chdir(ROOT_DIR) do - sh("flutter", "build", "ios", "--config-only", "--release") + sh("flutter", "build", "ios", "--config-only", "--release", *build_defines) end original_path = ENV.fetch("PATH", "") diff --git a/lib/helper/utils/build_info.dart b/lib/helper/utils/build_info.dart new file mode 100644 index 0000000..be7c56b --- /dev/null +++ b/lib/helper/utils/build_info.dart @@ -0,0 +1,13 @@ +/// Build metadata injected at compile time via `--dart-define`; both are +/// empty in local development builds. +abstract final class BuildInfo { + /// Full commit hash of the build, or empty when not injected. + static const commit = String.fromEnvironment('BUILD_COMMIT'); + + /// UTC build timestamp in ISO 8601, or empty when not injected. + static const time = String.fromEnvironment('BUILD_TIME'); + + /// Short commit hash for display. + static String get shortCommit => + commit.length > 7 ? commit.substring(0, 7) : commit; +} diff --git a/lib/views/applets/settings/widgets/other_settings_card.dart b/lib/views/applets/settings/widgets/other_settings_card.dart index ed418ca..9e63de7 100644 --- a/lib/views/applets/settings/widgets/other_settings_card.dart +++ b/lib/views/applets/settings/widgets/other_settings_card.dart @@ -4,6 +4,7 @@ import 'package:canokey_console/generated/l10n.dart'; import 'package:canokey_console/helper/utils/logging.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; import 'package:canokey_console/helper/theme/theme_customizer.dart'; +import 'package:canokey_console/helper/utils/build_info.dart'; import 'package:canokey_console/helper/utils/icp_filing.dart'; import 'package:canokey_console/helper/utils/ui_mixins.dart'; import 'package:canokey_console/helper/widgets/customized_text.dart'; @@ -160,6 +161,16 @@ class _OtherSettingsCardState extends State with UIMixin { ), applicationLegalese: '© 2026 canokeys.org', children: [ + if (BuildInfo.commit.isNotEmpty) ...[ + Padding( + padding: Spacing.y(8), + child: SelectableText( + 'commit ${BuildInfo.shortCommit}' + '${BuildInfo.time.isEmpty ? '' : ' · ${BuildInfo.time}'}', + style: CustomizedTextStyle.bodySmall(), + ), + ), + ], Padding( padding: Spacing.y(8), child: CustomizedText.bodyMedium( From f7ea0cf0d69d74f5fbd865a792346cf4478246d0 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 21:25:13 +0800 Subject: [PATCH 12/19] feat: skip the duplicate serial read in probes libcanokey's probe now accepts the bootstrap-observed serial; SmartCard records it on the lease at connection setup and every client's prepare passes it through, removing one serial read per refresh. --- docs/libcanokey-migration.md | 4 +- lib/helper/utils/admin_card.dart | 4 +- lib/helper/utils/card_session.dart | 17 +++++++ lib/helper/utils/oath_card.dart | 4 +- lib/helper/utils/openpgp_card.dart | 4 +- lib/helper/utils/pass_card.dart | 4 +- lib/helper/utils/piv_card.dart | 2 +- lib/helper/utils/smartcard.dart | 15 +++++- lib/src/rust/api/protocol.dart | 17 +++++-- lib/src/rust/frb_generated.dart | 26 +++++++--- rust/Cargo.lock | 20 ++++---- rust/Cargo.toml | 4 +- rust/src/api/protocol.rs | 67 ++++++++++++++++++++++---- rust/src/frb_generated.rs | 11 +++-- test/helper/utils/admin_card_test.dart | 34 +++++++++++++ 15 files changed, 188 insertions(+), 45 deletions(-) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 70a940e..3ba7de1 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -24,7 +24,9 @@ building, UI flows, credential prompting) stays in Console. - `lib/helper/utils/smartcard.dart` — the `SmartCard.process` queue; every use case (connect, SELECT, authenticate, commands, cleanup) runs inside it. Connection bootstrap identity commands use upstream `admin::command` - builders via `ProtocolOperation.bootstrapIdentity`. + builders via `ProtocolOperation.bootstrapIdentity`, and the serial they + read is recorded on the lease so later probes skip the duplicate serial + read (`observedSerial`). ## Contracts that must not be broken diff --git a/lib/helper/utils/admin_card.dart b/lib/helper/utils/admin_card.dart index ba5a631..406afcf 100644 --- a/lib/helper/utils/admin_card.dart +++ b/lib/helper/utils/admin_card.dart @@ -108,7 +108,9 @@ class AdminCardClient extends AdminSessionCardClient { /// Explicit minimal discovery, before any authentication. Repeat after a /// profile-affecting write, including an uncertain write; never auto-reprobe. - Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + Future prepare() => prepareProfile( + () => ProtocolOperation.probeAdmin(observedSerial: lease.bootstrapSerial), + ); /// Explicit discovery only when the prepared binding is missing or stale. /// A still-valid binding is reused as-is, preserving the lease's recorded diff --git a/lib/helper/utils/card_session.dart b/lib/helper/utils/card_session.dart index 6a998a0..8713ce6 100644 --- a/lib/helper/utils/card_session.dart +++ b/lib/helper/utils/card_session.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; /// A local cancellation request. Only the executor closes its Rust operation; /// cancellation never races it to free a handle or abort an in-flight exchange. @@ -149,6 +150,22 @@ class CardLease { _adminAuthProfile = null; } + Uint8List? _bootstrapSerial; + + /// The four-byte serial read by this lease's connection bootstrap. Probes + /// record it as their observation and skip the duplicate serial read. + /// Only the transport owner may call this, right after bootstrapping. + void recordBootstrapSerial(Uint8List serial) { + check(); + if (serial.length != 4) { + throw ArgumentError.value(serial, 'serial', 'Serial must be 4 bytes'); + } + _bootstrapSerial = Uint8List.fromList(serial); + } + + /// The bootstrap-observed serial, if this lease's connection read one. + Uint8List? get bootstrapSerial => _bootstrapSerial; + /// Reserve the whole protocol operation, including gaps between exchanges. /// Competing work fails before I/O rather than sharing selection/auth state. CardOperationLease beginOperation() { diff --git a/lib/helper/utils/oath_card.dart b/lib/helper/utils/oath_card.dart index 02e80bd..145a67b 100644 --- a/lib/helper/utils/oath_card.dart +++ b/lib/helper/utils/oath_card.dart @@ -69,7 +69,9 @@ class OathCardClient extends ProfileCardClient { /// Explicit minimal discovery, before any authentication. Repeat after a /// profile-affecting write, including an uncertain write; never auto-reprobe. - Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + Future prepare() => prepareProfile( + () => ProtocolOperation.probeAdmin(observedSerial: lease.bootstrapSerial), + ); /// All OATH operations SELECT the applet before their target command. A /// non-protocol failure discards the profile evidence. diff --git a/lib/helper/utils/openpgp_card.dart b/lib/helper/utils/openpgp_card.dart index 2a2716c..ebeaaf1 100644 --- a/lib/helper/utils/openpgp_card.dart +++ b/lib/helper/utils/openpgp_card.dart @@ -20,7 +20,9 @@ class OpenPgpCardClient extends ProfileCardClient { /// Explicit minimal discovery before any OpenPGP operation. The profile only /// carries firmware observations; upstream operations SELECT OpenPGP themselves. - Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + Future prepare() => prepareProfile( + () => ProtocolOperation.probeAdmin(observedSerial: lease.bootstrapSerial), + ); /// Every upstream OpenPGP operation SELECTs its applet first. Future _execute( diff --git a/lib/helper/utils/pass_card.dart b/lib/helper/utils/pass_card.dart index cda800a..3e561c1 100644 --- a/lib/helper/utils/pass_card.dart +++ b/lib/helper/utils/pass_card.dart @@ -15,7 +15,9 @@ class PassCardClient extends AdminSessionCardClient { /// Explicit minimal Admin discovery, before any slot operation. Repeat after /// a slot write, including an uncertain write; never auto-reprobe. - Future prepare() => prepareProfile(ProtocolOperation.probeAdmin); + Future prepare() => prepareProfile( + () => ProtocolOperation.probeAdmin(observedSerial: lease.bootstrapSerial), + ); /// Reads both slots. INS 43 sits behind the firmware Admin-PIN gate on /// every audited firmware: a still-valid recorded Admin authentication is diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index e8214ae..fc2b394 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -31,7 +31,7 @@ class PivCardClient extends ProfileCardClient { final override = _prepareExecutor; if (override != null) return override(); await prepareProfile( - ProtocolOperation.probePiv, + () => ProtocolOperation.probePiv(observedSerial: lease.bootstrapSerial), rejectWhileExchanging: true, ); } diff --git a/lib/helper/utils/smartcard.dart b/lib/helper/utils/smartcard.dart index ddbf2ea..8afeecd 100644 --- a/lib/helper/utils/smartcard.dart +++ b/lib/helper/utils/smartcard.dart @@ -72,6 +72,9 @@ class SmartCard { (throw StateError('Protocol operations require SmartCard.process')); static String _currentSN = ''; + // The serial read while probing a CCID candidate happens before any session + // binds; it is attached to each lease bound for that same physical card. + static Uint8List? _ccidBootstrapSerial; static CcidCard? _ccidCard; static bool _connectionQuarantined = false; @@ -455,6 +458,7 @@ class SmartCard { ), transport, ); + currentLease.recordBootstrapSerial(serial); final sn = hex.encode(serial).toUpperCase(); _currentSN = sn; if (isWeb()) { @@ -573,6 +577,7 @@ class SmartCard { if (session == null) return; final card = _ccidCard; final ccid = connectionType == ConnectionType.ccid; + final ccidBootstrapSerial = ccid ? _ccidBootstrapSerial : null; session.bind((command) async { try { log.d('C-APDU: $command'); @@ -589,7 +594,10 @@ class SmartCard { _sessions.invalidate(); _connectionQuarantined = true; if (ccid) { - if (identical(_ccidCard, card)) _ccidCard = null; + if (identical(_ccidCard, card)) { + _ccidCard = null; + _ccidBootstrapSerial = null; + } _connectionQuarantined = !await _disconnectCcidCard(card); } else { try { @@ -608,6 +616,9 @@ class SmartCard { rethrow; } }); + if (ccidBootstrapSerial != null) { + session.lease.recordBootstrapSerial(ccidBootstrapSerial); + } } static Future transceive(String capdu) async { @@ -666,6 +677,7 @@ class SmartCard { _sessions.invalidate(); await _disconnectCcidCard(activeCard); _ccidCard = null; + _ccidBootstrapSerial = null; if (connectionType == ConnectionType.ccid) { _currentSN = ''; connectionType = ConnectionType.none; @@ -702,6 +714,7 @@ class SmartCard { ); _ccidCard = candidate; + _ccidBootstrapSerial = Uint8List.fromList(serial); _currentSN = hex.encode(serial).toUpperCase(); connectionType = ConnectionType.ccid; if (isAndroidApp()) { diff --git a/lib/src/rust/api/protocol.dart b/lib/src/rust/api/protocol.dart index ee0a36a..ac1eff2 100644 --- a/lib/src/rust/api/protocol.dart +++ b/lib/src/rust/api/protocol.dart @@ -148,14 +148,21 @@ abstract class ProtocolOperation implements RustOpaqueInterface { static ProtocolOperation pivRead({required PivReadOperation kind}) => RustLib.instance.api.crateApiProtocolProtocolOperationPivRead(kind: kind); - /// Admin-only discovery before authentication; does not select PIV. - static ProtocolOperation probeAdmin() => - RustLib.instance.api.crateApiProtocolProtocolOperationProbeAdmin(); + /// Admin-only discovery before authentication; does not select PIV. A + /// four-byte serial already observed by the connection bootstrap is + /// recorded as the probe observation and the serial read is skipped. + static ProtocolOperation probeAdmin({Uint8List? observedSerial}) => + RustLib.instance.api.crateApiProtocolProtocolOperationProbeAdmin( + observedSerial: observedSerial, + ); /// Explicit Admin/PIV discovery. Call before authentication in the same /// exclusive lease; the completed profile is transferred to Dart exactly once. - static ProtocolOperation probePiv() => - RustLib.instance.api.crateApiProtocolProtocolOperationProbePiv(); + /// A bootstrap-observed serial skips the duplicate serial read. + static ProtocolOperation probePiv({Uint8List? observedSerial}) => + RustLib.instance.api.crateApiProtocolProtocolOperationProbePiv( + observedSerial: observedSerial, + ); /// Start exactly once; subsequent calls report an operation-state error. ProtocolStep start(); diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 824eca5..907688e 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -169,9 +169,13 @@ abstract class RustLibApi extends BaseApi { required PivReadOperation kind, }); - ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin(); + ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin({ + Uint8List? observedSerial, + }); - ProtocolOperation crateApiProtocolProtocolOperationProbePiv(); + ProtocolOperation crateApiProtocolProtocolOperationProbePiv({ + Uint8List? observedSerial, + }); ProtocolStep crateApiProtocolProtocolOperationStart({ required ProtocolOperation that, @@ -1495,11 +1499,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin() { + ProtocolOperation crateApiProtocolProtocolOperationProbeAdmin({ + Uint8List? observedSerial, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_opt_list_prim_u_8_strict(observedSerial, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!; }, codec: SseCodec( @@ -1508,7 +1515,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: null, ), constMeta: kCrateApiProtocolProtocolOperationProbeAdminConstMeta, - argValues: [], + argValues: [observedSerial], apiImpl: this, ), ); @@ -1517,15 +1524,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiProtocolProtocolOperationProbeAdminConstMeta => const TaskConstMeta( debugName: "ProtocolOperation_probe_admin", - argNames: [], + argNames: ["observedSerial"], ); @override - ProtocolOperation crateApiProtocolProtocolOperationProbePiv() { + ProtocolOperation crateApiProtocolProtocolOperationProbePiv({ + Uint8List? observedSerial, + }) { return handler.executeSync( SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_opt_list_prim_u_8_strict(observedSerial, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; }, codec: SseCodec( @@ -1534,7 +1544,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: null, ), constMeta: kCrateApiProtocolProtocolOperationProbePivConstMeta, - argValues: [], + argValues: [observedSerial], apiImpl: this, ), ); @@ -1543,7 +1553,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiProtocolProtocolOperationProbePivConstMeta => const TaskConstMeta( debugName: "ProtocolOperation_probe_piv", - argNames: [], + argNames: ["observedSerial"], ); @override diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9b7176a..01a5fd0 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,7 +355,7 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-admin", "canokey-compat", @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-compat", "canokey-protocol", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-protocol", ] @@ -387,7 +387,7 @@ dependencies = [ [[package]] name = "canokey-ctap" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "aes 0.8.4", "canokey-protocol", @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-compat", "canokey-protocol", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "canokey-ndef" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-protocol", ] @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-compat", "canokey-protocol", @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "canokey-compat", "canokey-key", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=41a3ea6059a58eb7a546e4a74bee891be5602bc0#41a3ea6059a58eb7a546e4a74bee891be5602bc0" +source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" dependencies = [ "thiserror 2.0.20", "zeroize", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 548c6ca..873b17d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "41a3ea6059a58eb7a546e4a74bee891be5602bc0", features = ["clientpin"] } -canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "41a3ea6059a58eb7a546e4a74bee891be5602bc0" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "fc314a81c30f19789795086394f9a5222ad300e8", features = ["clientpin"] } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "fc314a81c30f19789795086394f9a5222ad300e8" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/rust/src/api/protocol.rs b/rust/src/api/protocol.rs index cff03e6..634512e 100644 --- a/rust/src/api/protocol.rs +++ b/rust/src/api/protocol.rs @@ -2598,15 +2598,26 @@ pub struct ProtocolOperation { } impl ProtocolOperation { - /// Admin-only discovery before authentication; does not select PIV. + /// Admin-only discovery before authentication; does not select PIV. A + /// four-byte serial already observed by the connection bootstrap is + /// recorded as the probe observation and the serial read is skipped. #[flutter_rust_bridge::frb(sync)] - pub fn probe_admin() -> Self { + pub fn probe_admin(observed_serial: Option>) -> Self { Self::from_operation( - canokey::probe_device(canokey::ProbeOptions { - mode: canokey::ProbeMode::Minimal, - ..Default::default() - }) - .map(Inner::Probe), + observed_serial + .map(|s| { + <[u8; 4]>::try_from(s.as_slice()) + .map_err(|_| Error::new(ErrorKind::InvalidArgument)) + }) + .transpose() + .and_then(|observed_serial| { + canokey::probe_device(canokey::ProbeOptions { + mode: canokey::ProbeMode::Minimal, + observed_serial, + ..Default::default() + }) + }) + .map(Inner::Probe), ) } @@ -2643,10 +2654,23 @@ impl ProtocolOperation { /// Explicit Admin/PIV discovery. Call before authentication in the same /// exclusive lease; the completed profile is transferred to Dart exactly once. + /// A bootstrap-observed serial skips the duplicate serial read. #[flutter_rust_bridge::frb(sync)] - pub fn probe_piv() -> Self { + pub fn probe_piv(observed_serial: Option>) -> Self { Self::from_operation( - canokey::probe_device(canokey::ProbeOptions::default()).map(Inner::Probe), + observed_serial + .map(|s| { + <[u8; 4]>::try_from(s.as_slice()) + .map_err(|_| Error::new(ErrorKind::InvalidArgument)) + }) + .transpose() + .and_then(|observed_serial| { + canokey::probe_device(canokey::ProbeOptions { + observed_serial, + ..Default::default() + }) + }) + .map(Inner::Probe), ) } @@ -3204,7 +3228,7 @@ mod tests { #[test] fn probe_transfers_profile_and_metadata_copies_its_evidence() { - let mut probe = ProtocolOperation::probe_piv(); + let mut probe = ProtocolOperation::probe_piv(None); assert_eq!( probe.start().command.unwrap(), [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0, 0] @@ -3240,6 +3264,29 @@ mod tests { ); } + #[test] + fn probe_skips_the_serial_read_when_bootstrap_observed() { + let mut probe = ProtocolOperation::probe_admin(Some(vec![1, 2, 3, 4])); + assert_eq!( + probe.start().command.unwrap(), + [0, 0xa4, 4, 0, 5, 0xf0, 0, 0, 0, 0, 0] + ); + let mut step = probe.advance(vec![0x90, 0]); + for response in [b"3.1.0\x90\0".to_vec(), b"CanoKey\x90\0".to_vec()] { + assert!(step.command.is_some()); + step = probe.advance(response); + } + // No serial read APDU was issued; the observation is recorded. + let profile = step.profile.unwrap(); + assert_eq!(profile.serial(), Some(vec![1, 2, 3, 4])); + + // A malformed observed serial fails at construction, before any I/O. + let mut invalid = ProtocolOperation::probe_admin(Some(vec![1, 2, 3])); + assert_eq!(invalid.start().error.unwrap().kind, "InvalidArgument"); + let mut invalid = ProtocolOperation::probe_piv(Some(vec![0; 5])); + assert_eq!(invalid.start().error.unwrap().kind, "InvalidArgument"); + } + #[test] fn metadata_keeps_card_errors_and_rejects_duplicate_fields() { let profile = metadata_profile(); diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index a7774e0..66b0af6 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -1030,10 +1030,12 @@ fn wire__crate__api__protocol__ProtocolOperation_probe_admin_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_observed_serial = >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = - Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_admin())?; + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_admin( + api_observed_serial, + ))?; std::result::Result::Ok(output_ok) })()) }, @@ -1060,9 +1062,12 @@ fn wire__crate__api__protocol__ProtocolOperation_probe_piv_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_observed_serial = >>::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_piv())?; + let output_ok = Ok::<_, ()>(crate::api::protocol::ProtocolOperation::probe_piv( + api_observed_serial, + ))?; std::result::Result::Ok(output_ok) })()) }, diff --git a/test/helper/utils/admin_card_test.dart b/test/helper/utils/admin_card_test.dart index ad3ee48..2096d6f 100644 --- a/test/helper/utils/admin_card_test.dart +++ b/test/helper/utils/admin_card_test.dart @@ -3,6 +3,7 @@ library; import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:canokey_console/helper/utils/admin_card.dart'; import 'package:canokey_console/helper/utils/card_session.dart'; import 'package:canokey_console/helper/utils/piv_card.dart'; @@ -29,6 +30,39 @@ void main() { }, ); + test( + 'probe skips the serial read when the bootstrap observed it', + () async { + final transport = _Transport([ + '9000', + '${hex.encode(utf8.encode('3.1.0'))}9000', + '43616E6F4B65799000', + ]); + final client = AdminCardClient(transport: transport); + await client.withSession(() async { + client.lease.recordBootstrapSerial(Uint8List.fromList([1, 2, 3, 4])); + await client.prepare(); + // No serial read: the bootstrap observation is the probe's evidence. + expect(transport.commands, ['${_select}00', '0031000000', '0031010000']); + expect(await client.readSerial(), '01020304'); + }); + }, + ); + + test( + 'bootstrap serial evidence requires exactly four bytes', + () async { + final transport = _Transport([]); + final client = AdminCardClient(transport: transport); + await client.withSession(() async { + expect( + () => client.lease.recordBootstrapSerial(Uint8List(3)), + throwsArgumentError, + ); + }); + }, + ); + test( 'configuration, flash and applet usage use upstream validated results', () async { From 77a9c970b514e772cee7adb9ebc56ee23fef27c3 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 22:00:21 +0800 Subject: [PATCH 13/19] fix: address review follow-ups - Keep PIV profile evidence valid across the 3.0.x gated read (6982) so the page refresh survives the defaults fallback. - Re-confirm the physical card with a real serial read before Admin PIN verification; bootstrap observations no longer stand in for it. - Drop the poll-time session invalidation that could kill in-flight Android NFC operations; rebinding a lease already invalidates. - Zero the OATH access key and WebAuthn PIN copies after use, restore graceful keyboard-keymap degradation, and complete setPinRetries with a failure result instead of propagating metadata-write errors. - Bump libcanokey to f94f40ac (test-only upstream cleanup). --- .../applets/oath/oath_controller.dart | 205 ++++++++++-------- .../applets/piv/piv_controller.dart | 40 ++-- .../applets/settings/settings_controller.dart | 39 +++- lib/controller/base/admin.dart | 5 +- lib/helper/utils/admin_card.dart | 16 ++ lib/helper/utils/piv_card.dart | 32 ++- lib/helper/utils/smartcard.dart | 4 +- lib/helper/utils/webauthn_card.dart | 46 ++-- rust/Cargo.lock | 20 +- rust/Cargo.toml | 4 +- test/helper/utils/piv_card_test.dart | 20 +- 11 files changed, 263 insertions(+), 168 deletions(-) diff --git a/lib/controller/applets/oath/oath_controller.dart b/lib/controller/applets/oath/oath_controller.dart index 41ad77e..3042130 100644 --- a/lib/controller/applets/oath/oath_controller.dart +++ b/lib/controller/applets/oath/oath_controller.dart @@ -62,11 +62,15 @@ class OathController extends PollingController { log.t('Call OathController.doRefreshData'); await SmartCard.process((String sn) async { final (authenticated, key) = await _authenticate(sn); - if (!authenticated) { - return; - } + try { + if (!authenticated) { + return; + } - await _refresh(key); + await _refresh(key); + } finally { + key?.fillRange(0, key.length, 0); + } }); } @@ -75,43 +79,47 @@ class OathController extends PollingController { log.t('Call OathController.addAccount'); await SmartCard.process((String sn) async { final (authenticated, key) = await _authenticate(sn); - if (!authenticated) { - return; - } - try { - await _client.put( - name: name, - secretHex: secretHex, - type: type, - algorithm: algo, - digits: digits, - requireTouch: requireTouch, - initialValue: initValue, - key: key, - ); - } on ProtocolException catch (e) { - if (e.details.statusWord == 0x6985) { - Prompts.showPrompt( - S.of(Get.context!).oathDuplicated, ContentThemeColor.danger); + if (!authenticated) { return; } - final sw = _client.lastStatusWord; - if (sw != null && Prompts.isStorageFull(sw)) { - Prompts.showPrompt( - S.of(Get.context!).storageFull, ContentThemeColor.danger); - return; + + try { + await _client.put( + name: name, + secretHex: secretHex, + type: type, + algorithm: algo, + digits: digits, + requireTouch: requireTouch, + initialValue: initValue, + key: key, + ); + } on ProtocolException catch (e) { + if (e.details.statusWord == 0x6985) { + Prompts.showPrompt( + S.of(Get.context!).oathDuplicated, ContentThemeColor.danger); + return; + } + final sw = _client.lastStatusWord; + if (sw != null && Prompts.isStorageFull(sw)) { + Prompts.showPrompt( + S.of(Get.context!).storageFull, ContentThemeColor.danger); + return; + } + rethrow; } - rethrow; - } - log.i('Successfully added $name'); + log.i('Successfully added $name'); - Navigator.pop(Get.context!); - Prompts.showPrompt( - S.of(Get.context!).oathAdded, ContentThemeColor.success, - forceSnackBar: true); + Navigator.pop(Get.context!); + Prompts.showPrompt( + S.of(Get.context!).oathAdded, ContentThemeColor.success, + forceSnackBar: true); - await _refresh(key); + await _refresh(key); + } finally { + key?.fillRange(0, key.length, 0); + } }); } @@ -168,27 +176,31 @@ class OathController extends PollingController { late String code; await SmartCard.process((String sn) async { final (authenticated, key) = await _authenticate(sn); - if (!authenticated) { - return; - } + try { + if (!authenticated) { + return; + } - String? challengeHex; - if (type == OathType.totp) { - int challenge = DateTime.now().millisecondsSinceEpoch ~/ 30000; - challengeHex = challenge.toRadixString(16).padLeft(16, '0'); + String? challengeHex; + if (type == OathType.totp) { + int challenge = DateTime.now().millisecondsSinceEpoch ~/ 30000; + challengeHex = challenge.toRadixString(16).padLeft(16, '0'); + } + final (digits, rawCode) = await _client.calculate( + name: name, + type: type, + challengeHex: challengeHex, + key: key, + ); + code = formatOathCode( + rawCode: rawCode, digits: digits, format: oathMap[name]!.format); + oathMap[name]!.code = code; + + _startTimer(); + update(); + } finally { + key?.fillRange(0, key.length, 0); } - final (digits, rawCode) = await _client.calculate( - name: name, - type: type, - challengeHex: challengeHex, - key: key, - ); - code = formatOathCode( - rawCode: rawCode, digits: digits, format: oathMap[name]!.format); - oathMap[name]!.code = code; - - _startTimer(); - update(); }); return code; } @@ -197,17 +209,22 @@ class OathController extends PollingController { log.t('Call OathController.delete'); await SmartCard.process((String sn) async { final (authenticated, key) = await _authenticate(sn); - if (!authenticated) { - return; - } + try { + if (!authenticated) { + return; + } - await _client.delete(name, key: key); - log.i('Successfully deleted $name'); + await _client.delete(name, key: key); + log.i('Successfully deleted $name'); - Navigator.pop(Get.context!); - Prompts.showPrompt(S.of(Get.context!).deleted, ContentThemeColor.success, - forceSnackBar: true); - await _refresh(key); + Navigator.pop(Get.context!); + Prompts.showPrompt( + S.of(Get.context!).deleted, ContentThemeColor.success, + forceSnackBar: true); + await _refresh(key); + } finally { + key?.fillRange(0, key.length, 0); + } }); } @@ -215,40 +232,44 @@ class OathController extends PollingController { log.t('Call OathController.setDefault'); await SmartCard.process((String sn) async { final (authenticated, key) = await _authenticate(sn); - if (!authenticated) { - return; - } - try { - // The dialog numbers slots from 1; the protocol numbers from 0. - await _client.setDefault( - name: name, - slot: slot - 1, - appendEnter: withEnter, - key: key, - ); - } on ProtocolException catch (e) { - // Legacy single-slot firmware rejects a long slot or an appended - // Enter before any I/O; the nameless 6984 reply maps to NotFound. - if (e.details.kind == 'InvalidArgument' && - e.details.phase == 'Construction') { - Prompts.showPrompt( - S.of(Get.context!).notSupported, ContentThemeColor.warning); + if (!authenticated) { return; } - if (e.details.kind == 'NotFound') { - Prompts.showPrompt( - S.of(Get.context!).operationFailed, ContentThemeColor.danger); - return; + + try { + // The dialog numbers slots from 1; the protocol numbers from 0. + await _client.setDefault( + name: name, + slot: slot - 1, + appendEnter: withEnter, + key: key, + ); + } on ProtocolException catch (e) { + // Legacy single-slot firmware rejects a long slot or an appended + // Enter before any I/O; the nameless 6984 reply maps to NotFound. + if (e.details.kind == 'InvalidArgument' && + e.details.phase == 'Construction') { + Prompts.showPrompt( + S.of(Get.context!).notSupported, ContentThemeColor.warning); + return; + } + if (e.details.kind == 'NotFound') { + Prompts.showPrompt( + S.of(Get.context!).operationFailed, ContentThemeColor.danger); + return; + } + rethrow; } - rethrow; - } - log.i('Successfully changed default'); + log.i('Successfully changed default'); - Navigator.pop(Get.context!); - Prompts.showPrompt( - S.of(Get.context!).successfullyChanged, ContentThemeColor.success, - forceSnackBar: true); + Navigator.pop(Get.context!); + Prompts.showPrompt( + S.of(Get.context!).successfullyChanged, ContentThemeColor.success, + forceSnackBar: true); + } finally { + key?.fillRange(0, key.length, 0); + } }); } diff --git a/lib/controller/applets/piv/piv_controller.dart b/lib/controller/applets/piv/piv_controller.dart index e0600d3..95041c8 100644 --- a/lib/controller/applets/piv/piv_controller.dart +++ b/lib/controller/applets/piv/piv_controller.dart @@ -159,17 +159,10 @@ class PivController extends PollingController { } Future _refreshCapabilities() async { - PivAlgorithmExtensionConfig? config; - try { - config = await _client.readAlgorithmExtensions(); - } on ProtocolException catch (error) { - // Console reads capabilities before any authentication; on 3.0.x the - // read sits behind management-key authentication (upstream capability - // PivProtectedAlgorithmConfigRead), so the card rejects it with 6982. - // That security rejection alone permits the firmware-defaults fallback; - // every other failure remains visible. - if (error.details.kind != 'SecurityStatusNotSatisfied') rethrow; - } + // The client maps an unavailable instruction and the 3.0.x management-key + // gate (6982, card state untouched) to null; every other failure remains + // visible. + final config = await _client.readAlgorithmExtensions(); if (config != null) { algorithmExtensionConfig = config; return; @@ -260,10 +253,7 @@ class PivController extends PollingController { } Future _readKeyMetadata(int slot) async { - return _client.readMetadata( - slot, - algorithmExtensionConfig: algorithmExtensionConfig, - ); + return _client.readMetadata(slot); } Future _readSlotCertificate(int slot, SlotInfo? slotInfo) async { @@ -820,14 +810,18 @@ class PivController extends PollingController { authenticateManagementKey: () => _authenticateManagementKey(managementKey), updateMetadata: () async { - final adminData = await _getDataObject(_pivmanDataObject); - if (adminData == null) return false; - final flags = _pivmanFlags(adminData); - if (flags & _pivmanPukBlockedFlag == 0) return true; - return _putDataObject( - _pivmanDataObject, - _buildPivmanData(adminData, flags & ~_pivmanPukBlockedFlag), - ); + try { + final adminData = await _getDataObject(_pivmanDataObject); + if (adminData == null) return false; + final flags = _pivmanFlags(adminData); + if (flags & _pivmanPukBlockedFlag == 0) return true; + return await _putDataObject( + _pivmanDataObject, + _buildPivmanData(adminData, flags & ~_pivmanPukBlockedFlag), + ); + } catch (_) { + return false; + } }, ), ); diff --git a/lib/controller/applets/settings/settings_controller.dart b/lib/controller/applets/settings/settings_controller.dart index 14d6202..348108b 100644 --- a/lib/controller/applets/settings/settings_controller.dart +++ b/lib/controller/applets/settings/settings_controller.dart @@ -1,6 +1,7 @@ import 'package:canokey_console/helper/utils/applet_switches.dart'; import 'package:canokey_console/helper/storage/local_storage.dart'; import 'dart:async'; +import 'dart:typed_data'; import 'package:canokey_console/controller/base/admin.dart'; import 'package:canokey_console/controller/base/polling_controller.dart'; @@ -413,15 +414,9 @@ class SettingsController extends PollingController with AdminApplet { } Future _readKeyboardKeymap(String pin) async { + final int? layoutId; try { - final layoutId = await _client.readKeyboardLayout(pin: pin); - final entries = await _client.readKeyboardKeymap(pin: pin); - return KeyboardKeymapState( - layoutId: layoutId, - entries: entries, - preset: KeyboardKeymapPresets.findMatching(layoutId, entries), - isDefault: false, - ); + layoutId = await _client.readKeyboardLayout(pin: pin); } on ProtocolException catch (error) { if (error.details.kind == 'NotFound') { return const KeyboardKeymapState( @@ -431,8 +426,34 @@ class SettingsController extends PollingController with AdminApplet { isDefault: true, ); } - rethrow; + // A one-off read failure degrades the keymap UI instead of failing the + // whole settings refresh. + log.w('Failed to read keyboard layout id', error: error); + return const KeyboardKeymapState( + layoutId: null, + entries: null, + preset: null, + isDefault: false, + ); } + final Uint8List entries; + try { + entries = await _client.readKeyboardKeymap(pin: pin); + } on ProtocolException catch (error) { + log.w('Failed to read keyboard keymap', error: error); + return KeyboardKeymapState( + layoutId: layoutId, + entries: null, + preset: KeyboardKeymapPresets.findById(layoutId), + isDefault: false, + ); + } + return KeyboardKeymapState( + layoutId: layoutId, + entries: entries, + preset: KeyboardKeymapPresets.findMatching(layoutId, entries), + isDefault: false, + ); } static const int _appletUsageRecordLengthBytes = 6; diff --git a/lib/controller/base/admin.dart b/lib/controller/base/admin.dart index e70e0f6..50dfb0e 100644 --- a/lib/controller/base/admin.dart +++ b/lib/controller/base/admin.dart @@ -173,7 +173,10 @@ mixin AdminApplet on GetxController { _forgetLeasePin(); final lease = SmartCard.currentLease; await adminCardClient.prepare(); - if (await adminCardClient.readSerial() != expectedSerial.toUpperCase()) { + // A real serial read, not the bootstrap-fed observation: this is the only + // point that can detect the card being swapped before authentication. + if (await adminCardClient.readSerialFromCard() != + expectedSerial.toUpperCase()) { throw StateError('Admin device changed before authentication'); } if (await adminCardClient.verifyPin(pin)) { diff --git a/lib/helper/utils/admin_card.dart b/lib/helper/utils/admin_card.dart index 406afcf..164966f 100644 --- a/lib/helper/utils/admin_card.dart +++ b/lib/helper/utils/admin_card.dart @@ -211,6 +211,22 @@ class AdminCardClient extends AdminSessionCardClient { (throw StateError('Device did not report its serial')), ) .toUpperCase(); + + /// A real serial read over the wire, unlike [readSerial] which returns the + /// probe (possibly bootstrap-fed) observation. Authentication re-confirms + /// the physical device with this before submitting a PIN. + Future readSerialFromCard() async { + final binding = preparedBinding; + final data = await executeProtocolOperation( + ProtocolOperation.bootstrapIdentity(step: BootstrapIdentityStep.serial), + transport, + lease: binding.lease, + cancellation: cancellation, + ); + binding.check(); + cancellation.check(); + return hex.encode(data).toUpperCase(); + } Future readChipId() async => hex.encode((await _read(AdminReadOperation.chipId)).data).toUpperCase(); diff --git a/lib/helper/utils/piv_card.dart b/lib/helper/utils/piv_card.dart index fc2b394..58f71ae 100644 --- a/lib/helper/utils/piv_card.dart +++ b/lib/helper/utils/piv_card.dart @@ -438,22 +438,39 @@ class PivCardClient extends ProfileCardClient { String? managementKey, AlgorithmType managementKeyAlgorithm = AlgorithmType.tdes, }) async { + // The keyed operation SELECTs PIV and authenticates management within one + // operation. A fresh prepared binding already implies PIV is the selected + // applet (the binding is selection-bound), so the internal re-SELECT does + // not disturb anyone else's evidence. await prepareIfStale(); final keyBytes = managementKey == null ? null : Uint8List.fromList(hex.decode(managementKey)); try { - final data = await _executePrepared( + final data = await executePrepared( (profile) => profile.pivReadAlgorithmConfig( managementKey: keyBytes, managementKeyAlgorithm: managementKeyAlgorithm.value, ), + verifyProfileIdentity: true, + discardOnOtherError: true, + discardOnProtocolError: (error) => + // The 3.0.x management-key gate rejects an unauthenticated read + // with 6982 without touching card state; the profile stays valid. + error.exchangeAttempted && + error.details.kind != 'AuthenticationFailed' && + error.details.kind != 'PinBlocked' && + error.details.kind != 'SecurityStatusNotSatisfied', ); return PivAlgorithmExtensionConfig.decode(data); } on ProtocolException catch (error) { - // Only an unavailable instruction permits the existing firmware fallback. - // Security, malformed-data and unexpected card failures remain errors. - if (error.details.kind == 'UnsupportedFeature') return null; + // An unavailable instruction (or the 3.0.x authentication gate on an + // unauthenticated read) permits the existing firmware defaults. Other + // security, malformed-data and unexpected card failures remain errors. + if (error.details.kind == 'UnsupportedFeature' || + error.details.kind == 'SecurityStatusNotSatisfied') { + return null; + } rethrow; } finally { keyBytes?.fillRange(0, keyBytes.length, 0); @@ -461,11 +478,8 @@ class PivCardClient extends ProfileCardClient { } /// Requires prepare() in this same lease. Algorithm interpretation comes from - /// its observed profile, never the caller's cached algorithmExtensionConfig. - Future readMetadata( - int slot, { - PivAlgorithmExtensionConfig? algorithmExtensionConfig, - }) async { + /// its observed profile, never caller-side cached UI configuration. + Future readMetadata(int slot) async { RangeError.checkValueInInterval(slot, 0, 0xff, 'slot'); final binding = preparedBinding; Uint8List data; diff --git a/lib/helper/utils/smartcard.dart b/lib/helper/utils/smartcard.dart index 8afeecd..d394d7b 100644 --- a/lib/helper/utils/smartcard.dart +++ b/lib/helper/utils/smartcard.dart @@ -204,7 +204,9 @@ class SmartCard { readIso15693: false, androidPlatformSound: false, ); - if (connectionType != ConnectionType.ccid) _sessions.invalidate(); + // A re-poll of the tag held in the field is the in-flight operation's + // own event (the process* states complete its completer below), so + // evidence is only invalidated when a new use case binds its lease. log.t('[nfcHandler] NFC tag polled: ${tag.id}'); switch (nfcState) { case NfcState.mute: diff --git a/lib/helper/utils/webauthn_card.dart b/lib/helper/utils/webauthn_card.dart index cdf5b05..801a3c9 100644 --- a/lib/helper/utils/webauthn_card.dart +++ b/lib/helper/utils/webauthn_card.dart @@ -130,14 +130,27 @@ class WebAuthnPinSession { int get protocolVersion => _session.protocolVersion(); - Future setPin(String newPin) async => - _client._execute(_session.setPin(newPin: utf8.encode(newPin))); + Future setPin(String newPin) async { + final newPinBytes = utf8.encode(newPin); + try { + await _client._execute(_session.setPin(newPin: newPinBytes)); + } finally { + newPinBytes.fillRange(0, newPinBytes.length, 0); + } + } - Future changePin(String oldPin, String newPin) async => - _client._execute( - _session.changePin( - oldPin: utf8.encode(oldPin), newPin: utf8.encode(newPin)), + Future changePin(String oldPin, String newPin) async { + final oldPinBytes = utf8.encode(oldPin); + final newPinBytes = utf8.encode(newPin); + try { + await _client._execute( + _session.changePin(oldPin: oldPinBytes, newPin: newPinBytes), ); + } finally { + oldPinBytes.fillRange(0, oldPinBytes.length, 0); + newPinBytes.fillRange(0, newPinBytes.length, 0); + } + } /// Mint a pinUvAuthToken (credentialManagement permission 0x04). The token /// outlives this session; close the session once the token exists. @@ -146,14 +159,19 @@ class WebAuthnPinSession { int permissions = WebAuthnCardClient.permissionCredentialManagement, String? rpId, }) async { - final token = await _client._executeToken( - _session.getPinTokenWithPermissions( - pin: utf8.encode(pin), - permissions: permissions, - rpId: rpId, - ), - ); - return WebAuthnPinToken._(_client, token); + final pinBytes = utf8.encode(pin); + try { + final token = await _client._executeToken( + _session.getPinTokenWithPermissions( + pin: pinBytes, + permissions: permissions, + rpId: rpId, + ), + ); + return WebAuthnPinToken._(_client, token); + } finally { + pinBytes.fillRange(0, pinBytes.length, 0); + } } void close() { diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 01a5fd0..f3f6aa7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,7 +355,7 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "canokey" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-admin", "canokey-compat", @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "canokey-admin" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "canokey-compat" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-protocol", ] @@ -387,7 +387,7 @@ dependencies = [ [[package]] name = "canokey-ctap" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "aes 0.8.4", "canokey-protocol", @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "canokey-key" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "canokey-ndef" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-protocol", ] @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "canokey-oath" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-compat", "canokey-protocol", @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "canokey-openpgp" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "canokey-compat", "canokey-key", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "canokey-piv" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "aes 0.8.4", "canokey-compat", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "canokey-protocol" version = "0.1.0" -source = "git+https://github.com/canokeys/libcanokey?rev=fc314a81c30f19789795086394f9a5222ad300e8#fc314a81c30f19789795086394f9a5222ad300e8" +source = "git+https://github.com/canokeys/libcanokey?rev=f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e#f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" dependencies = [ "thiserror 2.0.20", "zeroize", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 873b17d..858f6f5 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "staticlib"] [dependencies] -canokey = { git = "https://github.com/canokeys/libcanokey", rev = "fc314a81c30f19789795086394f9a5222ad300e8", features = ["clientpin"] } -canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "fc314a81c30f19789795086394f9a5222ad300e8" } +canokey = { git = "https://github.com/canokeys/libcanokey", rev = "f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e", features = ["clientpin"] } +canokey-protocol = { git = "https://github.com/canokeys/libcanokey", rev = "f94f40acaf5bb57906d2edab1fd1b73d0cfdd97e" } aes = "^0.9.3" const-oid = "^0.10.1" der = { version = "^0.8.1", features = ["derive", "pem"] } diff --git a/test/helper/utils/piv_card_test.dart b/test/helper/utils/piv_card_test.dart index 371aff1..7849ceb 100644 --- a/test/helper/utils/piv_card_test.dart +++ b/test/helper/utils/piv_card_test.dart @@ -646,9 +646,9 @@ void main() { ); test( - 'algorithm configuration propagates security and malformed replies', + 'algorithm configuration propagates malformed replies and survives the gate', () async { - for (final response in ['6982', '6F00', '019000']) { + for (final response in ['6F00', '019000']) { final transport = _QueueApduTransport([response]); await _withPreparedClient(transport, (client) async { await expectLater( @@ -658,6 +658,15 @@ void main() { expect(transport.commands, ['00EE010000']); }); } + + // The 3.0.x management-key gate (6982) falls back to defaults and keeps + // the profile evidence: dependent reads continue without re-discovery. + final transport = _QueueApduTransport(['6982', '9000']); + await _withPreparedClient(transport, (client) async { + expect(await client.readAlgorithmExtensions(), isNull); + expect(await client.readPinRetries(), '9000'); + expect(transport.commands, ['00EE010000', '0020008000']); + }); }, ); @@ -858,7 +867,7 @@ void main() { }); test( - 'metadata algorithms use this device profile instead of cached UI IDs', + 'metadata algorithms resolve through this device profile', () async { final responses = [..._probeResponses.take(6)]; responses[1] = '322E302E309000'; @@ -869,10 +878,7 @@ void main() { final client = PivCardClient(transport: transport); await client.withSession(() async { await client.prepare(); - final metadata = await client.readMetadata( - 0x9a, - algorithmExtensionConfig: PivAlgorithmExtensionConfig.defaults, - ); + final metadata = await client.readMetadata(0x9a); expect(metadata, isNotNull); expect(metadata!.algorithm, AlgorithmType.ed25519); }); From b4ec54d762c82e5f85990d86bde218dd0f85a71b Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 22:07:52 +0800 Subject: [PATCH 14/19] feat: polish the build info in the About dialog The commit hash links to the GitHub commit page and the build time is shown in local time, both with localized labels. --- lib/generated/intl/messages_en.dart | 2 ++ lib/generated/intl/messages_zh_Hans.dart | 2 ++ lib/generated/intl/messages_zh_Hant.dart | 2 ++ lib/generated/l10n.dart | 10 +++++++ lib/helper/utils/build_info.dart | 16 ++++++++++ lib/l10n/intl_en.arb | 2 ++ lib/l10n/intl_zh_Hans.arb | 2 ++ lib/l10n/intl_zh_Hant.arb | 2 ++ .../settings/widgets/other_settings_card.dart | 30 ++++++++++++++++--- 9 files changed, 64 insertions(+), 4 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index f682f2a..68ba90e 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -104,6 +104,8 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About"), + "buildCommit": MessageLookupByLibrary.simpleMessage("Commit"), + "buildTime": MessageLookupByLibrary.simpleMessage("Built"), "actions": MessageLookupByLibrary.simpleMessage("Actions"), "add": MessageLookupByLibrary.simpleMessage("Add"), "agreeAndContinue": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh_Hans.dart b/lib/generated/intl/messages_zh_Hans.dart index ecdc4b2..f18faac 100644 --- a/lib/generated/intl/messages_zh_Hans.dart +++ b/lib/generated/intl/messages_zh_Hans.dart @@ -95,6 +95,8 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("关于"), + "buildCommit": MessageLookupByLibrary.simpleMessage("提交"), + "buildTime": MessageLookupByLibrary.simpleMessage("构建时间"), "actions": MessageLookupByLibrary.simpleMessage("操作"), "add": MessageLookupByLibrary.simpleMessage("添加"), "agreeAndContinue": MessageLookupByLibrary.simpleMessage("同意并继续"), diff --git a/lib/generated/intl/messages_zh_Hant.dart b/lib/generated/intl/messages_zh_Hant.dart index d40680c..c5c48c8 100644 --- a/lib/generated/intl/messages_zh_Hant.dart +++ b/lib/generated/intl/messages_zh_Hant.dart @@ -95,6 +95,8 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("關於"), + "buildCommit": MessageLookupByLibrary.simpleMessage("提交"), + "buildTime": MessageLookupByLibrary.simpleMessage("建置時間"), "actions": MessageLookupByLibrary.simpleMessage("操作"), "add": MessageLookupByLibrary.simpleMessage("新增"), "agreeAndContinue": MessageLookupByLibrary.simpleMessage("同意並繼續"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index b0505a5..e7398fb 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -74,6 +74,16 @@ class S { return Intl.message('About', name: 'about', desc: '', args: []); } + /// `Commit` + String get buildCommit { + return Intl.message('Commit', name: 'buildCommit', desc: '', args: []); + } + + /// `Built` + String get buildTime { + return Intl.message('Built', name: 'buildTime', desc: '', args: []); + } + /// `CanoKey Console` String get homeScreenTitle { return Intl.message( diff --git a/lib/helper/utils/build_info.dart b/lib/helper/utils/build_info.dart index be7c56b..3e5c0da 100644 --- a/lib/helper/utils/build_info.dart +++ b/lib/helper/utils/build_info.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; + /// Build metadata injected at compile time via `--dart-define`; both are /// empty in local development builds. abstract final class BuildInfo { @@ -10,4 +12,18 @@ abstract final class BuildInfo { /// Short commit hash for display. static String get shortCommit => commit.length > 7 ? commit.substring(0, 7) : commit; + + /// The build time in the local timezone, e.g. `2026-09-16 21:48`, or empty + /// when not injected or unparseable. + static String get localTime { + final parsed = DateTime.tryParse(time); + if (parsed == null) return ''; + return DateFormat('yyyy-MM-dd HH:mm').format(parsed.toLocal()); + } + + /// The GitHub page of the exact commit, or empty when not injected. + static String get commitUrl => + commit.isEmpty + ? '' + : 'https://github.com/canokeys/canokey-console/commit/$commit'; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 58dfe31..414c830 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4,6 +4,8 @@ "settings": "Settings", "other": "Other", "about": "About", + "buildCommit": "Commit", + "buildTime": "Built", "homeScreenTitle": "CanoKey Console", "homePress": "Press", "homeSelect": "to select an applet", diff --git a/lib/l10n/intl_zh_Hans.arb b/lib/l10n/intl_zh_Hans.arb index c485e37..f6237c2 100644 --- a/lib/l10n/intl_zh_Hans.arb +++ b/lib/l10n/intl_zh_Hans.arb @@ -4,6 +4,8 @@ "settings": "设置", "other": "其他", "about": "关于", + "buildCommit": "提交", + "buildTime": "构建时间", "homeScreenTitle": "CanoKey Console", "homePress": "点击", "homeSelect": "选择应用", diff --git a/lib/l10n/intl_zh_Hant.arb b/lib/l10n/intl_zh_Hant.arb index 73b22e2..b31982a 100644 --- a/lib/l10n/intl_zh_Hant.arb +++ b/lib/l10n/intl_zh_Hant.arb @@ -4,6 +4,8 @@ "settings": "設定", "other": "其他", "about": "關於", + "buildCommit": "提交", + "buildTime": "建置時間", "homeScreenTitle": "CanoKey Console", "homePress": "點選", "homeSelect": "選擇應用程式", diff --git a/lib/views/applets/settings/widgets/other_settings_card.dart b/lib/views/applets/settings/widgets/other_settings_card.dart index 9e63de7..d819c06 100644 --- a/lib/views/applets/settings/widgets/other_settings_card.dart +++ b/lib/views/applets/settings/widgets/other_settings_card.dart @@ -46,6 +46,7 @@ class _OtherSettingsCardState extends State with UIMixin { late final TapGestureRecognizer _privacyPolicyRecognizer; late final TapGestureRecognizer _feedbackEmailRecognizer; late final TapGestureRecognizer _icpFilingRecognizer; + late final TapGestureRecognizer _commitRecognizer; @override void initState() { @@ -61,12 +62,15 @@ class _OtherSettingsCardState extends State with UIMixin { _launchUrl(Uri(scheme: 'mailto', path: _feedbackEmail)); _icpFilingRecognizer = TapGestureRecognizer() ..onTap = () => _launchUrl(Uri.parse(icpFilingUrl)); + _commitRecognizer = TapGestureRecognizer() + ..onTap = () => _launchUrl(Uri.parse(BuildInfo.commitUrl)); _initPackageInfo(); } @override void dispose() { _repoRecognizer.dispose(); + _commitRecognizer.dispose(); _privacyPolicyRecognizer.dispose(); _feedbackEmailRecognizer.dispose(); _icpFilingRecognizer.dispose(); @@ -164,13 +168,31 @@ class _OtherSettingsCardState extends State with UIMixin { if (BuildInfo.commit.isNotEmpty) ...[ Padding( padding: Spacing.y(8), - child: SelectableText( - 'commit ${BuildInfo.shortCommit}' - '${BuildInfo.time.isEmpty ? '' : ' · ${BuildInfo.time}'}', - style: CustomizedTextStyle.bodySmall(), + child: RichText( + text: TextSpan( + children: [ + TextSpan(text: '${S.of(context).buildCommit} '), + TextSpan( + text: BuildInfo.shortCommit, + style: TextStyle( + color: contentTheme.primary, + decoration: TextDecoration.underline, + ), + recognizer: _commitRecognizer, + ), + ], + style: CustomizedTextStyle.bodySmall(), + ), ), ), ], + if (BuildInfo.localTime.isNotEmpty) + Padding( + padding: Spacing.y(8), + child: CustomizedText.bodySmall( + '${S.of(context).buildTime} ${BuildInfo.localTime}', + ), + ), Padding( padding: Spacing.y(8), child: CustomizedText.bodyMedium( From 1a8cf0e6389c14cc7b0d80163755d10f5bfebd96 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 22:13:29 +0800 Subject: [PATCH 15/19] docs: note the remaining wire redundancies needing upstream support --- docs/libcanokey-migration.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/libcanokey-migration.md b/docs/libcanokey-migration.md index 3ba7de1..7b24a80 100644 --- a/docs/libcanokey-migration.md +++ b/docs/libcanokey-migration.md @@ -91,6 +91,19 @@ building, UI flows, credential prompting) stays in Console. and IV are generated by the facade's CSPRNG. Non-zero CTAP status bytes are data for the UI, not transport errors. +## Remaining wire-level redundancies (need upstream support) + +- `probe_device` always SELECTs Admin even when the connection bootstrap just + did; a probe option to skip selection on an already-selected channel would + remove one SELECT per refresh. +- `admin::operation_with_access` rejects `VerifyPin` under `Access::Existing` + at construction, so the authentication step re-SELECTs after the probe + left Admin selected. Allowing VERIFY on the existing selection (or a + dedicated selected-verify request) would remove another. +- The bootstrap-observed serial is recorded as probe evidence without a + card-side check by design; Console compensates with a real serial read + immediately before PIN verification. + ## Verification ```sh From 0e3fc3d657ef8116045710934350ec657e3677eb Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 22:25:09 +0800 Subject: [PATCH 16/19] fix: use the PR head commit for BUILD_COMMIT GITHUB_SHA on pull_request events is a temporary merge commit that exists on no branch, so the About dialog's commit link 404'd. Prefer github.event.pull_request.head.sha (and the event payload in the fastlane lanes). --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- android/fastlane/Fastfile | 9 ++++++++- ios/fastlane/Fastfile | 9 ++++++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bec0ac6..de910e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -154,7 +154,7 @@ jobs: - name: Run flutter release build (${{ matrix.target }}) run: | flutter build ${{ matrix.target }} --release ${{ matrix.build-args }} --verbose \ - --dart-define=BUILD_COMMIT="$GITHUB_SHA" \ + --dart-define=BUILD_COMMIT="${{ github.event.pull_request.head.sha || github.sha }}" \ --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" if [ "${{ matrix.target }}" == "linux" ]; then sudo sh -c 'umask 022; exec /snap/bin/snapcraft pack --destructive-mode --platform "$1"' _ "${{ matrix.arch }}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5810033..e66334d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,7 +49,7 @@ jobs: cargo install wasm-pack --version "$WASM_PACK_VERSION" --locked flutter_rust_bridge_codegen build-web --release \ --wasm-pack-rustup-toolchain "$RUST_NIGHTLY" - - run: flutter build web --verbose --dart-define=BUILD_COMMIT="$GITHUB_SHA" --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + - run: flutter build web --verbose --dart-define=BUILD_COMMIT="${{ github.event.pull_request.head.sha || github.sha }}" --dart-define=BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - uses: actions/upload-artifact@v7 with: name: web-spa diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index e89487e..c277367 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -9,7 +9,14 @@ ANDROID_METADATA_DIR = File.join(__dir__, "metadata", "android") # Compile-time build metadata shown in the About dialog. def build_defines - commit = ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + commit = + if ENV["GITHUB_EVENT_NAME"] == "pull_request" && ENV["GITHUB_EVENT_PATH"] + # GITHUB_SHA is a temporary merge commit on pull_request events. + require "json" + JSON.parse(File.read(ENV["GITHUB_EVENT_PATH"])).dig("pull_request", "head", "sha") + else + ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + end time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") ["--dart-define=BUILD_COMMIT=#{commit}", "--dart-define=BUILD_TIME=#{time}"] end diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 0ae9aae..c233e34 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -8,7 +8,14 @@ IOS_METADATA_DIR = File.join(__dir__, "metadata") # Compile-time build metadata shown in the About dialog. def build_defines - commit = ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + commit = + if ENV["GITHUB_EVENT_NAME"] == "pull_request" && ENV["GITHUB_EVENT_PATH"] + # GITHUB_SHA is a temporary merge commit on pull_request events. + require "json" + JSON.parse(File.read(ENV["GITHUB_EVENT_PATH"])).dig("pull_request", "head", "sha") + else + ENV.fetch("GITHUB_SHA") { `git rev-parse HEAD`.strip } + end time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") ["--dart-define=BUILD_COMMIT=#{commit}", "--dart-define=BUILD_TIME=#{time}"] end From c03ea2ee7240a1b6d7e39c4e8a5eca70c8a3f4d2 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Wed, 16 Sep 2026 22:25:30 +0800 Subject: [PATCH 17/19] feat: bundle app fonts and subset Noto Sans SC for CJK Bundle IBM Plex Sans, Poppins, Raleway, ABeeZee and Abel as assets and add tool/subset_cjk_font.py to build the CanoKey CJK subset, replacing runtime font downloads and the snap font shims. --- assets/fonts/ABeeZee-Regular.ttf | Bin 0 -> 44828 bytes assets/fonts/Abel-Regular.ttf | Bin 0 -> 32672 bytes assets/fonts/CanoKeyCJK-Bold.ttf | Bin 0 -> 3090776 bytes assets/fonts/CanoKeyCJK-Regular.ttf | Bin 0 -> 3093672 bytes assets/fonts/GoogleFonts-OFL.txt | 93 +++++++++++++ assets/fonts/IBMPlexSans-Bold.ttf | Bin 0 -> 205220 bytes assets/fonts/IBMPlexSans-Light.ttf | Bin 0 -> 205864 bytes assets/fonts/IBMPlexSans-Medium.ttf | Bin 0 -> 205292 bytes assets/fonts/IBMPlexSans-Regular.ttf | Bin 0 -> 205252 bytes assets/fonts/IBMPlexSans-SemiBold.ttf | Bin 0 -> 205284 bytes assets/fonts/NotoSansSC-OFL.txt | 93 +++++++++++++ assets/fonts/Poppins-Bold.ttf | Bin 0 -> 150292 bytes assets/fonts/Poppins-Light.ttf | Bin 0 -> 156188 bytes assets/fonts/Poppins-Medium.ttf | Bin 0 -> 152860 bytes assets/fonts/Poppins-Regular.ttf | Bin 0 -> 154628 bytes assets/fonts/Poppins-SemiBold.ttf | Bin 0 -> 151516 bytes assets/fonts/Raleway-ExtraBold.ttf | Bin 0 -> 138856 bytes lib/helper/theme/app_fonts.dart | 2 +- lib/helper/theme/snap_fonts.dart | 1 - lib/helper/theme/snap_fonts_io.dart | 33 ----- lib/helper/theme/snap_fonts_stub.dart | 1 - lib/helper/widgets/customized_text_style.dart | 2 +- lib/main.dart | 6 +- pubspec.yaml | 44 +++++++ snap/snapcraft.yaml | 1 - .../widgets/customized_text_style_test.dart | 2 +- tool/subset_cjk_font.py | 123 ++++++++++++++++++ 27 files changed, 359 insertions(+), 42 deletions(-) create mode 100644 assets/fonts/ABeeZee-Regular.ttf create mode 100644 assets/fonts/Abel-Regular.ttf create mode 100644 assets/fonts/CanoKeyCJK-Bold.ttf create mode 100644 assets/fonts/CanoKeyCJK-Regular.ttf create mode 100644 assets/fonts/GoogleFonts-OFL.txt create mode 100644 assets/fonts/IBMPlexSans-Bold.ttf create mode 100644 assets/fonts/IBMPlexSans-Light.ttf create mode 100644 assets/fonts/IBMPlexSans-Medium.ttf create mode 100644 assets/fonts/IBMPlexSans-Regular.ttf create mode 100644 assets/fonts/IBMPlexSans-SemiBold.ttf create mode 100644 assets/fonts/NotoSansSC-OFL.txt create mode 100644 assets/fonts/Poppins-Bold.ttf create mode 100644 assets/fonts/Poppins-Light.ttf create mode 100644 assets/fonts/Poppins-Medium.ttf create mode 100644 assets/fonts/Poppins-Regular.ttf create mode 100644 assets/fonts/Poppins-SemiBold.ttf create mode 100644 assets/fonts/Raleway-ExtraBold.ttf delete mode 100644 lib/helper/theme/snap_fonts.dart delete mode 100644 lib/helper/theme/snap_fonts_io.dart delete mode 100644 lib/helper/theme/snap_fonts_stub.dart create mode 100644 tool/subset_cjk_font.py diff --git a/assets/fonts/ABeeZee-Regular.ttf b/assets/fonts/ABeeZee-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a72265047081a6d5ce6d23f96486dba5d406711f GIT binary patch literal 44828 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 z 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/CanoKeyCJK-Bold.ttf b/assets/fonts/CanoKeyCJK-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..af9cd6715f7693a60feb9a162267934f655d584b GIT binary patch literal 3090776 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