From 1428110d60da99db66ef8b00f19c6cea5de94ee9 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Tue, 16 Jan 2024 13:45:56 +0800 Subject: [PATCH] add pcsc_example --- example/pcsc_example.dart | 61 +++++++++++++++++++ lib/fido2.dart | 2 + lib/src/ctap2/base.dart | 19 +++++- lib/src/ctap2/pin.dart | 39 +++++++++--- pubspec.yaml | 3 +- test/fido2_base_test.dart | 6 +- .../{fido2_ctap_test.dart => fido2_ctap.dart} | 0 test/fido2_pin_test.dart | 4 +- 8 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 example/pcsc_example.dart rename test/{fido2_ctap_test.dart => fido2_ctap.dart} (100%) diff --git a/example/pcsc_example.dart b/example/pcsc_example.dart new file mode 100644 index 0000000..cec62f4 --- /dev/null +++ b/example/pcsc_example.dart @@ -0,0 +1,61 @@ +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:dart_pcsc/dart_pcsc.dart'; +import 'package:fido2/fido2.dart'; + +class CtapCcid extends CtapDevice { + final Card _card; + + CtapCcid(this._card); + + @override + Future>> transceive(List command) async { + List lc; + if (command.length <= 255) { + lc = [command.length]; + } else { + lc = [0, command.length >> 8, command.length & 0xff]; + } + List capdu = [0x80, 0x10, 0x00, 0x00, ...lc, ...command]; + List rapdu = List.empty(); + do { + if (rapdu.length >= 2) { + var remain = rapdu[rapdu.length - 1]; + capdu = [0x80, 0xC0, 0x00, 0x00, remain]; + rapdu = rapdu.sublist(0, rapdu.length - 2); + } + rapdu += await _card.transmit(Uint8List.fromList(capdu)); + } while (rapdu.length >= 2 && rapdu[rapdu.length - 2] == 0x61); + return CtapResponse(rapdu[0], rapdu.sublist(1, rapdu.length - 2)); + } +} + +void main() async { + final context = Context(Scope.user); + try { + await context.establish(); + + Card card = await context.connect( + 'Canokeys Canokey', + ShareMode.shared, + Protocol.any, + ); + + Uint8List resp = await card.transmit( + Uint8List.fromList(hex.decode('00A4040008A0000006472F0001')), + ); + int status = (resp[resp.length - 2] << 8) + resp[resp.length - 1]; + print('Status: 0x${status.toRadixString(16)}'); + + CtapDevice device = CtapCcid(card); + final ctap = await Ctap2.create(device); + print(ctap.info.versions); + final cp = await ClientPin.create(ctap); + print(await cp.getPinRetries()); + + await card.disconnect(Disposition.resetCard); + } finally { + await context.release(); + } +} diff --git a/lib/fido2.dart b/lib/fido2.dart index d8ddff7..0175cc4 100644 --- a/lib/fido2.dart +++ b/lib/fido2.dart @@ -2,3 +2,5 @@ library fido2; export 'src/ctap2/base.dart'; export 'src/ctap2/pin.dart'; +export 'src/ctap.dart'; +export 'src/cose.dart'; diff --git a/lib/src/ctap2/base.dart b/lib/src/ctap2/base.dart index a8b2c64..19b174a 100644 --- a/lib/src/ctap2/base.dart +++ b/lib/src/ctap2/base.dart @@ -107,11 +107,24 @@ class ClientPinResponse { } class Ctap2 { - CtapDevice device; + late final AuthenticatorInfo _info; + final CtapDevice device; - Ctap2(this.device); + Ctap2._create(this.device); - Future> getInfo() async { + static Future create(CtapDevice device) async { + final ctap2 = Ctap2._create(device); + final res = await ctap2.refreshInfo(); + if (res.status != 0) { + throw Exception('GetInfo failed.'); + } + ctap2._info = res.data; + return ctap2; + } + + AuthenticatorInfo get info => _info; + + Future> refreshInfo() async { final req = makeGetInfoRequest(); final res = await device.transceive(req); return CtapResponse(res.status, parseGetInfoResponse(res.data)); diff --git a/lib/src/ctap2/pin.dart b/lib/src/ctap2/pin.dart index 45a0f42..ded0b25 100644 --- a/lib/src/ctap2/pin.dart +++ b/lib/src/ctap2/pin.dart @@ -6,7 +6,6 @@ import 'package:cryptography/helpers.dart'; import 'package:elliptic/ecdh.dart'; import 'package:elliptic/elliptic.dart'; import 'package:fido2/fido2.dart'; -import 'package:fido2/src/cose.dart'; import 'package:quiver/collection.dart'; class EncapsulateResult { @@ -190,7 +189,7 @@ class ClientPin { return cp; } - var res = await ctap.getInfo(); + var res = await ctap.refreshInfo(); if (res.status != 0) { throw Exception('GetInfo failed.'); } @@ -209,6 +208,16 @@ class ClientPin { return cp; } + /// Returns true if the authenticator [info] supports the ClientPin command. + static bool isSupported(AuthenticatorInfo info) { + return info.options?.containsKey('clientPin') ?? false; + } + + /// Returns true if the authenticator [info] supports the pinUvAuthToken option. + static bool isTokenSupported(AuthenticatorInfo info) { + return info.options?.containsKey('pinUvAuthToken') ?? false; + } + Future _getSharedSecret() async { final resp = await _ctap.clientPin(ClientPinRequest( pinUvAuthProtocol: _pinProtocol.version, @@ -219,25 +228,41 @@ class ClientPin { return _pinProtocol.encapsulate(resp.data.keyAgreement!); } + /// Get a PIN/UV token from the authenticator. + /// + /// [pin] is the PIN code. + /// [permissions] is the permissions to be granted to the token. + /// [permissionsRpId] is the RP ID to which the permissions apply. Future> getPinToken(String pin, - List? permission, String? permissionsRpId) async { + {List? permissions, String? permissionsRpId}) async { final EncapsulateResult ss = await _getSharedSecret(); final pinHash = (await Sha256().hash(utf8.encode(pin))).bytes.sublist(0, 16); final pinHashEnc = await _pinProtocol.encrypt(ss.sharedSecret, pinHash); - // TODO check permissions + int subCmd = ClientPinSubCommand.getPinToken.value; + if (ClientPin.isTokenSupported(_ctap.info)) { + subCmd = + ClientPinSubCommand.getPinUvAuthTokenUsingPinWithPermissions.value; + } final resp = await _ctap.clientPin(ClientPinRequest( pinUvAuthProtocol: _pinProtocol.version, - subCommand: ClientPinSubCommand.getPinToken.value, + subCommand: subCmd, keyAgreement: ss.coseKey, pinHashEnc: pinHashEnc, - permissions: permission?.fold(0, (p, e) => p! | e.value), + permissions: permissions?.fold(0, (p, e) => p! | e.value), rpId: permissionsRpId)); - // TODO: validate pin token return await _pinProtocol.decrypt( ss.sharedSecret, resp.data.pinUvAuthToken!); } + + /// Get the number of PIN retries remaining. + Future getPinRetries() async { + final resp = await _ctap.clientPin(ClientPinRequest( + pinUvAuthProtocol: _pinProtocol.version, + subCommand: ClientPinSubCommand.getPinRetries.value)); + return resp.data.pinRetries!; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 475419c..3d45dc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,5 +14,6 @@ dependencies: quiver: ^3.2.1 dev_dependencies: - lints: ^2.1.0 + lints: ^3.0.0 test: ^1.24.0 + dart_pcsc: ^2.0.0 diff --git a/test/fido2_base_test.dart b/test/fido2_base_test.dart index bc0870f..598c83e 100644 --- a/test/fido2_base_test.dart +++ b/test/fido2_base_test.dart @@ -4,7 +4,7 @@ import 'package:fido2/src/cose.dart'; import 'package:fido2/src/ctap.dart'; import 'package:test/test.dart'; -import 'fido2_ctap_test.dart'; +import 'fido2_ctap.dart'; void main() { group('AuthenticatorInfo', () { @@ -23,8 +23,8 @@ void main() { test('With Device', () async { MockDevice device = MockDevice(); - Ctap2 ctap2 = Ctap2(device); - CtapResponse resp = await ctap2.getInfo(); + Ctap2 ctap2 = await Ctap2.create(device); + CtapResponse resp = await ctap2.refreshInfo(); expect(resp.status, equals(0)); expect(resp.data, isA()); }); diff --git a/test/fido2_ctap_test.dart b/test/fido2_ctap.dart similarity index 100% rename from test/fido2_ctap_test.dart rename to test/fido2_ctap.dart diff --git a/test/fido2_pin_test.dart b/test/fido2_pin_test.dart index bb653df..de73fb0 100644 --- a/test/fido2_pin_test.dart +++ b/test/fido2_pin_test.dart @@ -6,7 +6,7 @@ import 'package:fido2/fido2.dart'; import 'package:fido2/src/cose.dart'; import 'package:test/test.dart'; -import 'fido2_ctap_test.dart'; +import 'fido2_ctap.dart'; void main() { group('Protocol 1', () { @@ -72,7 +72,7 @@ void main() { group('ClientPin', () { test('Constructor', () async { MockDevice device = MockDevice(); - Ctap2 ctap2 = Ctap2(device); + Ctap2 ctap2 = await Ctap2.create(device); ClientPin cp = await ClientPin.create(ctap2); expect(cp.pinProtocolVersion, 1); });