Skip to content

Commit

Permalink
add pcsc_example
Browse files Browse the repository at this point in the history
  • Loading branch information
dangfan committed Jan 16, 2024
1 parent 26213f8 commit 1428110
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 16 deletions.
61 changes: 61 additions & 0 deletions example/pcsc_example.dart
Original file line number Diff line number Diff line change
@@ -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<CtapResponse<List<int>>> transceive(List<int> command) async {
List<int> lc;
if (command.length <= 255) {
lc = [command.length];
} else {
lc = [0, command.length >> 8, command.length & 0xff];
}
List<int> capdu = [0x80, 0x10, 0x00, 0x00, ...lc, ...command];
List<int> 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();
}
}
2 changes: 2 additions & 0 deletions lib/fido2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ library fido2;

export 'src/ctap2/base.dart';
export 'src/ctap2/pin.dart';
export 'src/ctap.dart';
export 'src/cose.dart';
19 changes: 16 additions & 3 deletions lib/src/ctap2/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<CtapResponse<AuthenticatorInfo>> getInfo() async {
static Future<Ctap2> 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<CtapResponse<AuthenticatorInfo>> refreshInfo() async {
final req = makeGetInfoRequest();
final res = await device.transceive(req);
return CtapResponse(res.status, parseGetInfoResponse(res.data));
Expand Down
39 changes: 32 additions & 7 deletions lib/src/ctap2/pin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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<EncapsulateResult> _getSharedSecret() async {
final resp = await _ctap.clientPin(ClientPinRequest(
pinUvAuthProtocol: _pinProtocol.version,
Expand All @@ -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<List<int>> getPinToken(String pin,
List<ClientPinPermission>? permission, String? permissionsRpId) async {
{List<ClientPinPermission>? 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<int> getPinRetries() async {
final resp = await _ctap.clientPin(ClientPinRequest(
pinUvAuthProtocol: _pinProtocol.version,
subCommand: ClientPinSubCommand.getPinRetries.value));
return resp.data.pinRetries!;
}
}
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions test/fido2_base_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand All @@ -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<AuthenticatorInfo>());
});
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions test/fido2_pin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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);
});
Expand Down

0 comments on commit 1428110

Please sign in to comment.