PCSC sample in Flutter/Dart

Here is a new PC/SC sample in Dart language I promised in PC/SC sample in different languages.

Fabien Rousseau wrote a PC/SC wrapper for Dart (the language) and Flutter (the UI toolkit).

The library is at https://pub.dev/packages/flutter_pcsc. It is available for GNU/Linux, macOS and Windows.

The source code is at https://github.com/fabienrousseau/flutter_pcsc and uses the MIT license. The current version is 0.0.4.

Fabien also provides an sample code at https://github.com/fabienrousseau/flutter_pcsc/blob/main/flutter_pcsc/example/lib/main.dart. I reused this code for my example bellow.

 

Installation

I just cloned the flutter_pcsc git repository.

You also need to install the flutter SDK to build and run the application. But if you use Flutter I imagine your already have the SDK installed.


Source code

You can use flutter create test to create a sample application with the correct structure and configuration files.

Then edit the pubspec.yaml file to add in the dependencies: section something like:

dependencies:
  flutter_pcsc:
    path: /home/rousseau/Documents/github/flutter_pcsc/flutter_pcsc

Of course you adapt the path to the location of your flutter_pcsc directory.

The source code of the sample application lib/main.dart is:

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';

import 'package:flutter_pcsc/flutter_pcsc.dart';

void main() {
  MyApp? myApp;

  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    FlutterError.onError = (FlutterErrorDetails details) {
      FlutterError.dumpErrorToConsole(details);
      myApp?.addError(details.toString());
    };

    runApp(myApp = MyApp());
  }, (Object error, StackTrace stack) {
    myApp?.addError(error.toString());
  });
}

class MyApp extends StatelessWidget {
  final GlobalKey<_MyAppBodyState> _myAppKey = GlobalKey();

  MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(
            title: const Text('PCSC example app'),
          ),
          body: MyAppBody(key: _myAppKey)),
    );
  }

  void addError(String msg) {
    _myAppKey.currentState?.messages.add(Message.error(msg));
  }
}

class MyAppBody extends StatefulWidget {
  const MyAppBody({required Key key}) : super(key: key);

  @override
  _MyAppBodyState createState() {
    return _MyAppBodyState();
  }
}

enum MessageType { info, error }

class Message {
  final String content;
  final MessageType type;
  Message(this.type, this.content);

  static info(String content) {
    return Message(MessageType.info, content);
  }

  static error(String content) {
    return Message(MessageType.error, content);
  }
}

class _MyAppBodyState extends State<MyAppBody> {
  static const List<int> selectAppletCommand = [
    0x00, 0xA4, 0x04, 0x00, 0x0A, 0xA0, 0x00, 0x00, 0x00, 0x62,
    0x03, 0x01, 0x0C, 0x06, 0x01
  ];
  static const List<int> appCommand = [
    0x00, 0x00, 0x00, 0x00
  ];
  final ScrollController _scrollController = ScrollController();

  final List<Message> messages = [];

  @override
  void initState() {
    super.initState();
    helloWorld();
  }

  Future<void> helloWorld() async {
    /* establish PCSC context */
    int ctx = await Pcsc.establishContext(PcscSCope.user);
    CardStruct? card;
    try {
      /* get the reader list */
      List<String> readers = await Pcsc.listReaders(ctx);

      if (readers.isEmpty) {
        messages.add(Message.error('Could not detect any reader'));
      } else {
        /* use the first reader */
        String reader = readers[0];
        setState(() {
          messages.add(Message.info('Using reader: $reader'));
        });

        /* connect to the card */
        card = await Pcsc.cardConnect(
            ctx, reader, PcscShare.shared, PcscProtocol.any);

        /* send select applet APDU */
        var response = await Pcsc.transmit(card, selectAppletCommand);
            messages.add(Message.info('Card returned: ${hexDump(response)}'));

        /* send applet test command */
        response = await Pcsc.transmit(card, appCommand);
        var sw = response.sublist(response.length - 2);
        var bytes = response.sublist(0, response.length - 2);
        var text = utf8.decode(bytes);

        messages.add(Message.info('Card returned: ${hexDump(response)}'));

        if (sw[0] != 0x90 || sw[1] != 0x00) {
          setState(() {
            messages
                .add(Message.error('Card returned an error: ${hexDump(sw)}'));
          });
        } else {
          setState(() {
            messages.add(Message.info('text is: ${text}'));
          });
        }
      }
    } finally {
      if (card != null) {
        try {
          /* disconnect from the card */
          await Pcsc.cardDisconnect(card.hCard, PcscDisposition.resetCard);
        } on Exception catch (e) {
          messages.add(Message.error(e.toString()));
        }
      }
      try {
        /* release PCSC context */
        await Pcsc.releaseContext(ctx);
      } on Exception catch (e) {
        messages.add(Message.error(e.toString()));
      }
    }
  }

  static String hexDump(List<int> csn) {
    return csn
        .map((i) => i.toRadixString(16).padLeft(2, '0').toUpperCase())
        .join(' ');
  }

  _scrollToBottom() {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
  }

  @override
  Widget build(BuildContext context) {
    TextStyle errorStyle = const TextStyle(color: Colors.red);
    WidgetsBinding.instance?.addPostFrameCallback((_) => _scrollToBottom());
    return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
      Expanded(
          child: Column(children: [
        Expanded(
            child: ListView(
                controller: _scrollController,
                children: messages
                    .map((e) => Text(e.content,
                        style: e.type == MessageType.error ? errorStyle : null))
                    .toList())),
        Container(
            margin: const EdgeInsets.all(10),
            child: ElevatedButton(
                onPressed: () async {
                  await tryAgain();
                },
                child: const Text("Try again")))
      ]))
    ]);
  }

  tryAgain() async {
    messages.clear();
    await helloWorld();
  }
}

Output

$ flutter run
hanging current working directory to: /home/rousseau/Documents/flutter/blog
Launching lib/main.dart on Linux in debug mode...
Building Linux application...                                           
Syncing files to device Linux...                                    55ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at:
http://127.0.0.1:34019/ORqpNzBQLEQ=/
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:34019/ORqpNzBQLEQ=/

Flutter is a framework to create graphical applications. So when you run the sample code you get a new window.

Remarks

The code is long because it is a complete Flutter application so you have to handle the graphical interface.

I asked Fabien for a Dart only interface but flutter_pcsc depends on some Flutter mechanisms to handle asynchronism. So it is not possible to write a PC/SC application for the console just using Dart. But if you use Dart I guess you also use flutter.

Despite of his (nice) name Fabien Rousseau is not part of my direct family. So I do not have a potential conflict of interest here 😜.


Conclusion

Thanks to Fabien for telling about his Dart/Flutter project.

If you work on a Free Software PC/SC wrapper that is not yet in my list please let me know.