Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions lib/api/nfc_fido_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
import 'package:twokey/api/fido_api.dart';
import 'package:twokey/common/app_logger.dart';
import 'package:twokey/common/system.dart' as system;

/// NFC implementation of the FIDO API using flutter_nfc_kit.
///
/// This class provides FIDO2 communication over NFC using the flutter_nfc_kit
/// package. It handles the transient nature of NFC connections by implementing
/// automatic reconnection strategies and proper lifecycle management.
///
/// Key features:
/// - Automatic NFC tag detection and FIDO2 application selection
/// - Robust error handling for transient connections
/// - Platform-aware availability checking (mobile devices only)
/// - Enhanced reconnection capabilities for lost connections
class NfcFidoApi implements FidoApi {
NFCTag? _tag;

@override
Future<void> connect() async {
// Check if NFC is available
final availability = await FlutterNfcKit.nfcAvailability;
if (availability != NFCAvailability.available) {
throw Exception('NFC is not available on this device');
}

// Poll for NFC tags
AppLogger.debug('Polling for NFC tags...');
try {
_tag = await FlutterNfcKit.poll(
timeout: Duration(seconds: 10),
iosMultipleTagMessage: "Multiple FIDO2 keys detected",
iosAlertMessage: "Hold your FIDO2 key near the device",
);
} catch (e) {
AppLogger.error('Failed to poll NFC tags: $e');
throw Exception(
'Failed to detect NFC tag. Please ensure your FIDO2 key is near the device and try again.',
);
}

if (_tag == null) {
throw Exception(
'No NFC tag found. Please hold your FIDO2 key near the device.',
);
}

AppLogger.debug('NFC tag detected: ${_tag!.type}, ID: ${_tag!.id}');

// Try to select the FIDO2 application
const fidoAid = 'A0000006472F0001';
final selectApdu = '00A4040008$fidoAid';
AppLogger.debug('--> SELECT FIDO App: $selectApdu');

final response = await FlutterNfcKit.transceive(selectApdu);
AppLogger.debug('<-- SELECT Response: $response');

if (!response.endsWith('9000')) {
await disconnect(); // Clean up the connection
throw Exception(
'Failed to select FIDO2 application. Response: $response',
);
}
}

@override
Future<void> disconnect() async {
if (_tag != null) {
await FlutterNfcKit.finish();
_tag = null;
AppLogger.debug('NFC connection closed');
}
}

@override
Future<Uint8List> transceive(Uint8List command) async {
if (_tag == null) {
throw Exception('NFC tag not connected');
}

try {
final commandHex = hex.encode(command);
AppLogger.debug('--> NFC Command: $commandHex');

final responseHex = await FlutterNfcKit.transceive(commandHex);

AppLogger.debug('<-- NFC Response: $responseHex');
return Uint8List.fromList(hex.decode(responseHex));
} catch (e) {
AppLogger.error('NFC transceive error: $e');
// For NFC, connection might be lost, so we should mark as disconnected
_tag = null;
rethrow;
}
}

/// Re-establish NFC connection if it was lost
Future<void> _reconnectIfNeeded() async {
if (_tag == null) {
await connect();
}
}

/// Enhanced transceive with automatic reconnection for transient NFC
Future<Uint8List> transceiveWithReconnect(Uint8List command) async {
try {
return await transceive(command);
} catch (e) {
AppLogger.debug('NFC transaction failed, attempting to reconnect: $e');
// Try to reconnect once for transient NFC connections
try {
await _reconnectIfNeeded();
return await transceive(command);
} catch (reconnectError) {
AppLogger.error('NFC reconnection failed: $reconnectError');
rethrow;
}
}
}

/// Check if NFC is available on the current device
static Future<bool> isAvailable() async {
try {
// NFC is primarily available on mobile devices
if (!system.isMobile()) {
AppLogger.debug('NFC not available: not a mobile platform');
return false;
}

final availability = await FlutterNfcKit.nfcAvailability;
final isAvailable = availability == NFCAvailability.available;
AppLogger.debug(
'NFC availability check: $availability (available: $isAvailable)',
);
return isAvailable;
} catch (e) {
AppLogger.debug('Error checking NFC availability: $e');
return false;
}
}

/// Poll for NFC tags without connecting to FIDO application
/// Useful for checking if tags are available
static Future<bool> hasTagsAvailable() async {
try {
final availability = await FlutterNfcKit.nfcAvailability;
if (availability != NFCAvailability.available) {
return false;
}

await FlutterNfcKit.finish(); // Clean up
return true;
} catch (e) {
AppLogger.debug('Error checking for NFC tags: $e');
return false;
}
}
}
187 changes: 187 additions & 0 deletions lib/api/unified_fido_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import 'dart:typed_data';
import 'package:twokey/api/fido_api.dart';
import 'package:twokey/api/ccid_fido_api.dart';
import 'package:twokey/api/nfc_fido_api.dart';
import 'package:twokey/common/app_logger.dart';
import 'package:ccid/ccid.dart';

/// Device type enumeration for FIDO2 authenticators
enum FidoDeviceType {
/// CCID (Chip Card Interface Device) smart card readers
ccid,

/// NFC (Near Field Communication) devices
nfc,
}

/// Information about an available FIDO2 device
class FidoDeviceInfo {
/// The type of device (CCID or NFC)
final FidoDeviceType type;

/// Human-readable name of the device
final String name;

/// Description of the device
final String description;

FidoDeviceInfo({
required this.type,
required this.name,
required this.description,
});
}

/// A unified FIDO API that can work with both CCID readers and NFC tags.
///
/// This class provides a single interface to interact with different types of
/// FIDO2 authenticators. It automatically detects available devices and allows
/// the user to select between them when multiple options are available.
///
/// Key features:
/// - Automatic device detection for both CCID and NFC
/// - Device selection when multiple authenticators are available
/// - Transparent handling of NFC transient connections with auto-reconnection
/// - Unified error handling across device types
class UnifiedFidoApi implements FidoApi {
FidoApi? _activeApi;
FidoDeviceType? _selectedDeviceType;

@override
Future<void> connect() async {
if (_selectedDeviceType == null) {
// Auto-detect and connect to the first available device
final availableDevices = await getAvailableDevices();
if (availableDevices.isEmpty) {
throw Exception(
'No FIDO2 devices found. Please ensure your key is connected or near the device.',
);
}

// Prefer CCID over NFC for stability if both are available
final preferredDevice = availableDevices.firstWhere(
(device) => device.type == FidoDeviceType.ccid,
orElse: () => availableDevices.first,
);

await connectToDevice(preferredDevice.type);
} else {
await connectToDevice(_selectedDeviceType!);
}
}

Future<void> connectToDevice(FidoDeviceType deviceType) async {
await disconnect(); // Ensure clean state

switch (deviceType) {
case FidoDeviceType.ccid:
_activeApi = CcidFidoApi();
break;
case FidoDeviceType.nfc:
_activeApi = NfcFidoApi();
break;
}

_selectedDeviceType = deviceType;
await _activeApi!.connect();
AppLogger.info('Connected to ${deviceType.name} device');
}

@override
Future<void> disconnect() async {
if (_activeApi != null) {
await _activeApi!.disconnect();
_activeApi = null;
}
_selectedDeviceType = null;
}

@override
Future<Uint8List> transceive(Uint8List command) async {
if (_activeApi == null) {
throw Exception('No device connected. Call connect() first.');
}

// For NFC, we need to handle transient connections with enhanced reconnection
if (_selectedDeviceType == FidoDeviceType.nfc && _activeApi is NfcFidoApi) {
return await (_activeApi as NfcFidoApi).transceiveWithReconnect(command);
}

// For CCID, use normal transceive
return await _activeApi!.transceive(command);
}

/// Get all available FIDO2 devices
Future<List<FidoDeviceInfo>> getAvailableDevices() async {
final devices = <FidoDeviceInfo>[];

// Check for CCID readers
try {
final ccid = Ccid();
final readers = await ccid.listReaders();
if (readers.isNotEmpty) {
for (final reader in readers) {
devices.add(
FidoDeviceInfo(
type: FidoDeviceType.ccid,
name: reader,
description: 'CCID Smart Card Reader',
),
);
}
AppLogger.info('Found ${readers.length} CCID reader(s)');
}
} catch (e) {
AppLogger.debug('Error checking CCID readers: $e');
}

// Check for NFC availability
try {
if (await NfcFidoApi.isAvailable()) {
devices.add(
FidoDeviceInfo(
type: FidoDeviceType.nfc,
name: 'NFC',
description: 'Near Field Communication',
),
);
AppLogger.info('NFC is available');
} else {
AppLogger.debug('NFC is not available on this device');
}
} catch (e) {
AppLogger.debug('Error checking NFC availability: $e');
}

AppLogger.info('Total available FIDO2 devices: ${devices.length}');
return devices;
}

/// Set preferred device type for connection
void setPreferredDeviceType(FidoDeviceType deviceType) {
_selectedDeviceType = deviceType;
}

/// Get currently connected device type
FidoDeviceType? get connectedDeviceType => _selectedDeviceType;

/// Get currently connected device type and name
String? get connectedDeviceName {
if (_activeApi == null || _selectedDeviceType == null) return null;

switch (_selectedDeviceType!) {
case FidoDeviceType.ccid:
return 'CCID Reader';
case FidoDeviceType.nfc:
return 'NFC Device';
}
}

/// Refresh and get all available FIDO2 devices
Future<List<FidoDeviceInfo>> refreshAvailableDevices() async {
return await getAvailableDevices();
}

/// Check if currently connected
bool get isConnected => _activeApi != null;
}
5 changes: 5 additions & 0 deletions lib/common/system.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import 'dart:io';

bool isDesktop() => Platform.isWindows || Platform.isLinux || Platform.isMacOS;

bool isMobile() => Platform.isAndroid || Platform.isIOS;

bool isWeb() =>
identical(0, 0.0); // This is a common way to detect web platform in Dart
4 changes: 2 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:twokey/api/ccid_fido_api.dart';
import 'package:twokey/api/unified_fido_api.dart';
import 'package:twokey/service/authenticator.dart';
import 'package:twokey/viewmodels/keys.dart';
import 'package:twokey/viewmodels/navigation.dart';
Expand Down Expand Up @@ -33,7 +33,7 @@ void main() {
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => KeysViewModel(AuthenticatorService(CcidFidoApi())),
create: (_) => KeysViewModel(AuthenticatorService(UnifiedFidoApi())),
),
ChangeNotifierProvider(create: (_) => NavigationViewModel()),
ChangeNotifierProvider(
Expand Down
Loading
Loading