cccd_vietnam 2.0.0 copy "cccd_vietnam: ^2.0.0" to clipboard
cccd_vietnam: ^2.0.0 copied to clipboard

Read and write data from ID card Viet Nam using NFC

example/lib/main.dart

// Created by Crt Vavros, copyright © 2022 ZeroPass. All rights reserved.
// ignore_for_file: prefer_adjacent_string_concatenation, prefer_interpolation_to_compose_strings


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

import 'package:cccd_vietnam/dmrtd.dart';
import 'package:cccd_vietnam/extensions.dart';
import 'package:flutter/services.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:logging/logging.dart';
import 'package:cccd_vietnam/src/proto/can_key.dart';
import 'package:intl/intl.dart';

import 'package:asn1lib/asn1lib.dart';
import 'mrz_scanner_screen.dart';

class MrtdData {
  EfCardAccess? cardAccess;
  EfCardSecurity? cardSecurity;
  EfCOM? com;
  EfSOD? sod;
  EfDG1? dg1;
  EfDG2? dg2;
  EfDG3? dg3;
  EfDG4? dg4;
  EfDG5? dg5;
  EfDG6? dg6;
  EfDG7? dg7;
  EfDG8? dg8;
  EfDG9? dg9;
  EfDG10? dg10;
  EfDG11? dg11;
  EfDG12? dg12;
  EfDG13? dg13;
  EfDG14? dg14;
  EfDG15? dg15;
  EfDG16? dg16;
  Uint8List? aaSig;
  bool? isPACE;
  bool? isDBA;
}

final Map<DgTag, String> dgTagToString = {
  EfDG1.TAG: 'EF.DG1',
  EfDG2.TAG: 'EF.DG2',
  EfDG3.TAG: 'EF.DG3',
  EfDG4.TAG: 'EF.DG4',
  EfDG5.TAG: 'EF.DG5',
  EfDG6.TAG: 'EF.DG6',
  EfDG7.TAG: 'EF.DG7',
  EfDG8.TAG: 'EF.DG8',
  EfDG9.TAG: 'EF.DG9',
  EfDG10.TAG: 'EF.DG10',
  EfDG11.TAG: 'EF.DG11',
  EfDG12.TAG: 'EF.DG12',
  EfDG13.TAG: 'EF.DG13',
  EfDG14.TAG: 'EF.DG14',
  EfDG15.TAG: 'EF.DG15',
  EfDG16.TAG: 'EF.DG16',
};

Widget _makeMrtdAccessDataWidget({
  required String header,
  required String collapsedText,
  required bool isPACE,
  required bool isDBA,
}) {
  return ExpandablePanel(
    theme: const ExpandableThemeData(
      headerAlignment: ExpandablePanelHeaderAlignment.center,
      tapBodyToCollapse: true,
      hasIcon: true,
      iconColor: Colors.red,
    ),
    header: Text(header),
    collapsed: Text(
      collapsedText,
      softWrap: true,
      maxLines: 2,
      overflow: TextOverflow.ellipsis,
    ),
    expanded: Container(
      padding: const EdgeInsets.all(18),
      color: Color.fromARGB(255, 239, 239, 239),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            'Access protocol: ${isPACE ? "PACE" : "BAC"}',
            //style: TextStyle(fontSize: 16.0),
          ),
          SizedBox(height: 8.0),
          Text(
            'Access key type: ${isDBA ? "DBA" : "CAN"}',
            //style: TextStyle(fontSize: 16.0),
          ),
        ],
      ),
    ),
  );
}

String formatEfCom(final EfCOM efCom) {
  var str =
      "version: ${efCom.version}\n"
      "unicode version: ${efCom.unicodeVersion}\n"
      "DG tags:";

  for (final t in efCom.dgTags) {
    try {
      str += " ${dgTagToString[t]!}";
    } catch (e) {
      str += " 0x${t.value.toRadixString(16)}";
    }
  }
  return str;
}

String formatMRZ(final MRZ mrz) {
  return "MRZ\n"
          "  version: ${mrz.version}\n" +
      "  doc code: ${mrz.documentCode}\n" +
      "  doc No.: ${mrz.documentNumber}\n" +
      "  country: ${mrz.country}\n" +
      "  nationality: ${mrz.nationality}\n" +
      "  name: ${mrz.firstName}\n" +
      "  surname: ${mrz.lastName}\n" +
      "  gender: ${mrz.gender}\n" +
      "  date of birth: ${DateFormat.yMd().format(mrz.dateOfBirth)}\n" +
      "  date of expiry: ${DateFormat.yMd().format(mrz.dateOfExpiry)}\n" +
      "  add. data: ${mrz.optionalData}\n" +
      "  add. data: ${mrz.optionalData2}";
}

String formatDG15(final EfDG15 dg15) {
  var str =
      "EF.DG15:\n"
      "  AAPublicKey\n"
      "    type: ";

  final rawSubPubKey = dg15.aaPublicKey.rawSubjectPublicKey();
  if (dg15.aaPublicKey.type == AAPublicKeyType.RSA) {
    final tvSubPubKey = TLV.fromBytes(rawSubPubKey);
    var rawSeq = tvSubPubKey.value;
    if (rawSeq[0] == 0x00) {
      rawSeq = rawSeq.sublist(1);
    }

    final tvKeySeq = TLV.fromBytes(rawSeq);
    final tvModule = TLV.decode(tvKeySeq.value);
    final tvExp = TLV.decode(tvKeySeq.value.sublist(tvModule.encodedLen));

    str +=
        "RSA\n"
        "    exponent: ${tvExp.value.hex()}\n"
        "    modulus: ${tvModule.value.hex()}";
  } else {
    str += "EC\n    SubjectPublicKey: ${rawSubPubKey.hex()}";
  }
  return str;
}

String formatProgressMsg(String message, int percentProgress) {
  final p = (percentProgress / 20).round();
  final full = "🟢 " * p;
  final empty = "⚪️ " * (5 - p);
  return message + "\n\n" + full + empty;
}

void main() {
  Logger.root.level = Level.ALL;
  Logger.root.logSensitiveData = true;
  Logger.root.onRecord.listen((record) {
    print(
      '${record.loggerName} ${record.level.name}: ${record.time}: ${record.message}',
    );
  });
  runApp(MrtdEgApp());
}

class MrtdEgApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PlatformApp(
      localizationsDelegates: [
        DefaultMaterialLocalizations.delegate,
        DefaultCupertinoLocalizations.delegate,
        DefaultWidgetsLocalizations.delegate,
      ],
      material: (_, __) => MaterialAppData(),
      cupertino: (_, __) => CupertinoAppData(),
      home: MrtdHomePage(),
    );
  }
}

class MrtdHomePage extends StatefulWidget {
  @override
  // ignore: library_private_types_in_public_api
  _MrtdHomePageState createState() => _MrtdHomePageState();
}

class _MrtdHomePageState extends State<MrtdHomePage>
    with TickerProviderStateMixin {
  var _alertMessage = "";
  final _log = Logger("mrtdeg.app");
  var _isNfcAvailable = false;
  var _isReading = false;
  final _mrzData = GlobalKey<FormState>();
  final _canData = GlobalKey<FormState>();

  // mrz data
  final _docNumber = TextEditingController(text: '001200004595');
  // final _docNumber = TextEditingController(text: '001200004');
  final _dob = TextEditingController(text: '04/27/2000'); // date of birth
  final _doe = TextEditingController(text: '04/27/2025');
  final _can = TextEditingController(text: '004595');
  bool _checkBoxPACE = false;

  MrtdData? _mrtdData;

  final NfcProvider _nfc = NfcProvider();

  // ignore: unused_field
  late Timer _timerStateUpdater;
  final _scrollController = ScrollController();
  late final TabController _tabController;

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

    _tabController = TabController(length: 2, vsync: this);
    //_tabController.addListener(_handleTabSelection);

    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);

    _initPlatformState();

    // Update platform state every 3 sec
    _timerStateUpdater = Timer.periodic(Duration(seconds: 3), (Timer t) {
      _initPlatformState();
    });
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> _initPlatformState() async {
    bool isNfcAvailable;
    try {
      NfcStatus status = await NfcProvider.nfcStatus;
      isNfcAvailable = status == NfcStatus.enabled;
    } on PlatformException {
      isNfcAvailable = false;
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _isNfcAvailable = isNfcAvailable;
    });
  }

  DateTime? _getDOBDate() {
    if (_dob.text.isEmpty) {
      return null;
    }
    return DateFormat.yMd().parse(_dob.text);
  }

  DateTime? _getDOEDate() {
    if (_doe.text.isEmpty) {
      return null;
    }
    return DateFormat.yMd().parse(_doe.text);
  }

  Future<String?> _pickDate(
    BuildContext context,
    DateTime firstDate,
    DateTime initDate,
    DateTime lastDate,
  ) async {
    final locale = Localizations.localeOf(context);
    final DateTime? picked = await showDatePicker(
      context: context,
      firstDate: firstDate,
      initialDate: initDate,
      lastDate: lastDate,
      locale: locale,
    );

    if (picked != null) {
      return DateFormat.yMd().format(picked);
    }
    return null;
  }

  void _buttonPressed() async {
    print("Button pressed");
    //Check on what tab we are
    if (_tabController.index == 0) {
      //DBA tab
      String errorText = "";
      if (_doe.text.isEmpty) {
        errorText += "Please enter date of expiry!\n";
      }
      if (_dob.text.isEmpty) {
        errorText += "Please enter date of birth!\n";
      }
      if (_docNumber.text.isEmpty) {
        errorText += "Please enter passport number!";
      }

      setState(() {
        _alertMessage = errorText;
      });
      //If there is an error, just jump out of the function
      if (errorText.isNotEmpty) return;

      final bacKeySeed = DBAKey(
        _docNumber.text,
        _getDOBDate()!,
        _getDOEDate()!,
        paceMode: _checkBoxPACE,
      );
      _readMRTD(accessKey: bacKeySeed, isPace: _checkBoxPACE);
    } else {
      //PACE tab
      String errorText = "";
      if (_can.text.isEmpty) {
        errorText = "Please enter CAN number!";
      } else if (_can.text.length != 6) {
        errorText = "CAN number must be exactly 6 digits long!";
      }

      setState(() {
        _alertMessage = errorText;
      });
      //If there is an error, just jump out of the function
      if (errorText.isNotEmpty) return;

      final canKeySeed = CanKey(_can.text);
      _readMRTD(accessKey: canKeySeed, isPace: true);
    }
  }

  void _readMRTD({required AccessKey accessKey, bool isPace = false}) async {
    try {
      setState(() {
        _mrtdData = null;
        _alertMessage = "Waiting for Passport tag ...";
        _isReading = true;
      });
      try {
        bool demo = false;
        if (!demo)
          await _nfc.connect(
            iosAlertMessage: "Hold your phone near Biometric Passport",
          );

        final passport = Passport(_nfc);

        setState(() {
          _alertMessage = "Reading Passport ...";
        });

        _nfc.setIosAlertMessage("Trying to read EF.CardAccess ...");
        final mrtdData = MrtdData();

        try {
          mrtdData.cardAccess = await passport.readEfCardAccess();
          print("CardAccess data: ${mrtdData.cardAccess?.toBytes().hex()}");
        } on PassportError catch (e) {
          print("Error reading CardAccess: $e");
          // Fall back to BAC if PACE fails
          isPace = false;
        } catch (e) {
          print("Unexpected error reading CardAccess: $e");
          isPace = false;
        }

        _nfc.setIosAlertMessage("Trying to read EF.CardSecurity ...");

        try {
          //mrtdData.cardSecurity = await passport.readEfCardSecurity();
        } on PassportError {
          //if (e.code != StatusWord.fileNotFound) rethrow;
        }

        _nfc.setIosAlertMessage("Initiating session...");
        //set MrtdData
        mrtdData.isPACE = isPace;
        mrtdData.isDBA = accessKey.PACE_REF_KEY_TAG == 0x01 ? true : false;

        if (isPace && mrtdData.cardAccess != null) {
          try {
            print("Attempting PACE session...");
            //PACE session
            await passport.startSessionPACE(accessKey, mrtdData.cardAccess!);
            print("PACE session successful");
          } catch (e) {
            print("PACE failed, falling back to BAC: $e");
            // Fall back to BAC
            isPace = false;
            mrtdData.isPACE = false;
            await passport.startSession(accessKey as DBAKey);
            print("BAC session successful");
          }
        } else {
          print("Using BAC session...");
          //BAC session

          print('docNumber: ${_docNumber.text}');
          print('dob: ${_getDOBDate()}');
          print('doe: ${_getDOEDate()}');
          final dbakey = DBAKey(
            _docNumber.text,
            _getDOBDate()!,
            _getDOEDate()!,
          );
          await passport.startSession(dbakey);
          print("BAC session successful");
        }

        _nfc.setIosAlertMessage(formatProgressMsg("Reading EF.COM ...", 0));
        mrtdData.com = await passport.readEfCOM();

        print("COM data: ${mrtdData.com?.toBytes().hex()}");

        _nfc.setIosAlertMessage(
          formatProgressMsg("Reading Data Groups ...", 20),
        );

        if (mrtdData.com!.dgTags.contains(EfDG1.TAG)) {
          mrtdData.dg1 = await passport.readEfDG1();
        }

        if (mrtdData.com!.dgTags.contains(EfDG2.TAG)) {
          mrtdData.dg2 = await passport.readEfDG2();
        }

        // To read DG3 and DG4 session has to be established with CVCA certificate (not supported).
        // if(mrtdData.com!.dgTags.contains(EfDG3.TAG)) {
        //   mrtdData.dg3 = await passport.readEfDG3();
        // }

        // if(mrtdData.com!.dgTags.contains(EfDG4.TAG)) {
        //   mrtdData.dg4 = await passport.readEfDG4();
        // }

        if (mrtdData.com!.dgTags.contains(EfDG5.TAG)) {
          mrtdData.dg5 = await passport.readEfDG5();
        }

        if (mrtdData.com!.dgTags.contains(EfDG6.TAG)) {
          mrtdData.dg6 = await passport.readEfDG6();
        }

        if (mrtdData.com!.dgTags.contains(EfDG7.TAG)) {
          mrtdData.dg7 = await passport.readEfDG7();
        }

        if (mrtdData.com!.dgTags.contains(EfDG8.TAG)) {
          mrtdData.dg8 = await passport.readEfDG8();
        }

        if (mrtdData.com!.dgTags.contains(EfDG9.TAG)) {
          mrtdData.dg9 = await passport.readEfDG9();
        }

        if (mrtdData.com!.dgTags.contains(EfDG10.TAG)) {
          mrtdData.dg10 = await passport.readEfDG10();
        }

        if (mrtdData.com!.dgTags.contains(EfDG11.TAG)) {
          mrtdData.dg11 = await passport.readEfDG11();
        }

        if (mrtdData.com!.dgTags.contains(EfDG12.TAG)) {
          mrtdData.dg12 = await passport.readEfDG12();
        }

        if (mrtdData.com!.dgTags.contains(EfDG13.TAG)) {
          mrtdData.dg13 = await passport.readEfDG13();
        }

        if (mrtdData.com!.dgTags.contains(EfDG14.TAG)) {
          mrtdData.dg14 = await passport.readEfDG14();
        }

        if (mrtdData.com!.dgTags.contains(EfDG15.TAG)) {
          mrtdData.dg15 = await passport.readEfDG15();
          _nfc.setIosAlertMessage(formatProgressMsg("Doing AA ...", 60));
          mrtdData.aaSig = await passport.activeAuthenticate(Uint8List(8));
        }

        if (mrtdData.com!.dgTags.contains(EfDG16.TAG)) {
          mrtdData.dg16 = await passport.readEfDG16();
        }

        _nfc.setIosAlertMessage(formatProgressMsg("Reading EF.SOD ...", 80));
        mrtdData.sod = await passport.readEfSOD();

        setState(() {
          _mrtdData = mrtdData;
        });

        setState(() {
          _alertMessage = "";
        });

        _scrollController.animateTo(
          300.0,
          duration: Duration(milliseconds: 500),
          curve: Curves.ease,
        );
      } on Exception catch (e) {
        final se = e.toString().toLowerCase();
        String alertMsg = "An error has occurred while reading Passport!";
        if (e is PassportError) {
          if (se.contains("security status not satisfied")) {
            alertMsg =
                "Failed to initiate session with passport.\nCheck input data!";
          }
          _log.error("PassportError: ${e.message}");
        } else {
          _log.error(
            "An exception was encountered while trying to read Passport: $e",
          );
        }

        if (se.contains('timeout')) {
          alertMsg = "Timeout while waiting for Passport tag";
        } else if (se.contains("tag was lost")) {
          alertMsg = "Tag was lost. Please try again!";
        } else if (se.contains("invalidated by user")) {
          alertMsg = "";
        }

        setState(() {
          _alertMessage = alertMsg;
        });
      } finally {
        if (_alertMessage.isNotEmpty) {
          await _nfc.disconnect(iosErrorMessage: _alertMessage);
        } else {
          await _nfc.disconnect(
            iosAlertMessage: formatProgressMsg("Finished", 100),
          );
        }
        setState(() {
          _isReading = false;
        });
      }
    } on Exception catch (e) {
      _log.error("Read MRTD error: $e");
    }
  }

  void _readMRTDOld() async {
    try {
      setState(() {
        _mrtdData = null;
        _alertMessage = "Waiting for Passport tag ...";
        _isReading = true;
      });

      await _nfc.connect(
        iosAlertMessage: "Hold your phone near Biometric Passport",
      );
      final passport = Passport(_nfc);

      setState(() {
        _alertMessage = "Reading Passport ...";
      });

      _nfc.setIosAlertMessage("Trying to read EF.CardAccess ...");
      final mrtdData = MrtdData();

      try {
        mrtdData.cardAccess = await passport.readEfCardAccess();
      } on PassportError {
        //if (e.code != StatusWord.fileNotFound) rethrow;
      }

      _nfc.setIosAlertMessage("Trying to read EF.CardSecurity ...");

      try {
        mrtdData.cardSecurity = await passport.readEfCardSecurity();
      } on PassportError {
        //if (e.code != StatusWord.fileNotFound) rethrow;
      }

      _nfc.setIosAlertMessage("Initiating session ...");
      final bacKeySeed = DBAKey(
        _docNumber.text,
        _getDOBDate()!,
        _getDOEDate()!,
      );
      await passport.startSession(bacKeySeed);

      _nfc.setIosAlertMessage(formatProgressMsg("Reading EF.COM ...", 0));
      mrtdData.com = await passport.readEfCOM();

      _nfc.setIosAlertMessage(formatProgressMsg("Reading Data Groups ...", 20));

      if (mrtdData.com!.dgTags.contains(EfDG1.TAG)) {
        mrtdData.dg1 = await passport.readEfDG1();
      }

      if (mrtdData.com!.dgTags.contains(EfDG2.TAG)) {
        mrtdData.dg2 = await passport.readEfDG2();
      }

      // To read DG3 and DG4 session has to be established with CVCA certificate (not supported).
      // if(mrtdData.com!.dgTags.contains(EfDG3.TAG)) {
      //   mrtdData.dg3 = await passport.readEfDG3();
      // }

      // if(mrtdData.com!.dgTags.contains(EfDG4.TAG)) {
      //   mrtdData.dg4 = await passport.readEfDG4();
      // }

      if (mrtdData.com!.dgTags.contains(EfDG5.TAG)) {
        mrtdData.dg5 = await passport.readEfDG5();
      }

      if (mrtdData.com!.dgTags.contains(EfDG6.TAG)) {
        mrtdData.dg6 = await passport.readEfDG6();
      }

      if (mrtdData.com!.dgTags.contains(EfDG7.TAG)) {
        mrtdData.dg7 = await passport.readEfDG7();
      }

      if (mrtdData.com!.dgTags.contains(EfDG8.TAG)) {
        mrtdData.dg8 = await passport.readEfDG8();
      }

      if (mrtdData.com!.dgTags.contains(EfDG9.TAG)) {
        mrtdData.dg9 = await passport.readEfDG9();
      }

      if (mrtdData.com!.dgTags.contains(EfDG10.TAG)) {
        mrtdData.dg10 = await passport.readEfDG10();
      }

      if (mrtdData.com!.dgTags.contains(EfDG11.TAG)) {
        mrtdData.dg11 = await passport.readEfDG11();
      }

      if (mrtdData.com!.dgTags.contains(EfDG12.TAG)) {
        mrtdData.dg12 = await passport.readEfDG12();
      }

      if (mrtdData.com!.dgTags.contains(EfDG13.TAG)) {
        mrtdData.dg13 = await passport.readEfDG13();
      }

      if (mrtdData.com!.dgTags.contains(EfDG14.TAG)) {
        mrtdData.dg14 = await passport.readEfDG14();
      }

      if (mrtdData.com!.dgTags.contains(EfDG15.TAG)) {
        mrtdData.dg15 = await passport.readEfDG15();
        _nfc.setIosAlertMessage(formatProgressMsg("Doing AA ...", 60));
        mrtdData.aaSig = await passport.activeAuthenticate(Uint8List(8));
      }

      if (mrtdData.com!.dgTags.contains(EfDG16.TAG)) {
        mrtdData.dg16 = await passport.readEfDG16();
      }

      _nfc.setIosAlertMessage(formatProgressMsg("Reading EF.SOD ...", 80));
      mrtdData.sod = await passport.readEfSOD();

      setState(() {
        _mrtdData = mrtdData;
      });

      setState(() {
        _alertMessage = "";
      });

      _scrollController.animateTo(
        300.0,
        duration: Duration(milliseconds: 500),
        curve: Curves.ease,
      );
    } on Exception catch (e) {
      final se = e.toString().toLowerCase();
      String alertMsg = "An error has occurred while reading Passport!";
      if (e is PassportError) {
        if (se.contains("security status not satisfied")) {
          alertMsg =
              "Failed to initiate session with passport.\nCheck input data!";
        }
        _log.error("PassportError: ${e.message}");
      } else {
        _log.error(
          "An exception was encountered while trying to read Passport: $e",
        );
      }

      if (se.contains('timeout')) {
        alertMsg = "Timeout while waiting for Passport tag";
      } else if (se.contains("tag was lost")) {
        alertMsg = "Tag was lost. Please try again!";
      } else if (se.contains("invalidated by user")) {
        alertMsg = "";
      }

      setState(() {
        _alertMessage = alertMsg;
      });
    } finally {
      if (_alertMessage.isNotEmpty) {
        await _nfc.disconnect(iosErrorMessage: _alertMessage);
      } else {
        await _nfc.disconnect(
          iosAlertMessage: formatProgressMsg("Finished", 100),
        );
      }
      setState(() {
        _isReading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return PlatformProvider(
      builder: (BuildContext context) => _buildPage(context),
    );
  }

  bool _disabledInput() {
    //return true;
    return _isReading || !_isNfcAvailable;
  }

  Widget _makeMrtdDataWidget({
    required String header,
    required String collapsedText,
    required dataText,
  }) {
    return ExpandablePanel(
      theme: const ExpandableThemeData(
        headerAlignment: ExpandablePanelHeaderAlignment.center,
        tapBodyToCollapse: true,
        hasIcon: true,
        iconColor: Colors.red,
      ),
      header: Text(header),
      collapsed: Text(
        collapsedText,
        softWrap: true,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
      ),
      expanded: Container(
        padding: const EdgeInsets.all(18),
        color: Color.fromARGB(255, 239, 239, 239),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            PlatformTextButton(
              child: Text('Copy'),
              onPressed: () => Clipboard.setData(ClipboardData(text: dataText)),
              padding: const EdgeInsets.all(8),
            ),
            SelectableText(dataText, textAlign: TextAlign.left),
          ],
        ),
      ),
    );
  }

  List<Widget> _mrtdDataWidgets() {
    List<Widget> list = [];
    if (_mrtdData == null) return list;

    if (_mrtdData!.isPACE != null && _mrtdData!.isDBA != null)
      list.add(
        _makeMrtdAccessDataWidget(
          header: "Access protocol",
          collapsedText: '',
          isDBA: _mrtdData!.isDBA!,
          isPACE: _mrtdData!.isPACE!,
        ),
      );

    if (_mrtdData!.cardAccess != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.CardAccess',
          collapsedText: '',
          dataText: _mrtdData!.cardAccess!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.cardSecurity != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.CardSecurity',
          collapsedText: '',
          dataText: _mrtdData!.cardSecurity!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.sod != null) {
      extractCertificatesFromSOD(_mrtdData!.sod!.toBytes());
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.SOD',
          collapsedText: '',
          dataText: base64Encode(_mrtdData!.sod!.toBytes()),
        ),
      );
    }

    if (_mrtdData!.com != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.COM',
          collapsedText: '',
          dataText: formatEfCom(_mrtdData!.com!),
        ),
      );
    }

    if (_mrtdData!.dg1 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG1',
          collapsedText: '',
          dataText: formatMRZ(_mrtdData!.dg1!.mrz),
        ),
      );
    }

    if (_mrtdData!.dg2 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG2',
          collapsedText: '',
          dataText: _mrtdData!.dg2!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg3 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG3',
          collapsedText: '',
          dataText: _mrtdData!.dg3!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg4 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG4',
          collapsedText: '',
          dataText: _mrtdData!.dg4!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg5 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG5',
          collapsedText: '',
          dataText: _mrtdData!.dg5!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg6 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG6',
          collapsedText: '',
          dataText: _mrtdData!.dg6!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg7 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG7',
          collapsedText: '',
          dataText: _mrtdData!.dg7!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg8 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG8',
          collapsedText: '',
          dataText: _mrtdData!.dg8!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg9 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG9',
          collapsedText: '',
          dataText: _mrtdData!.dg9!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg10 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG10',
          collapsedText: '',
          dataText: _mrtdData!.dg10!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg11 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG11',
          collapsedText: '',
          dataText: _mrtdData!.dg11!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg12 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG12',
          collapsedText: '',
          dataText: _mrtdData!.dg12!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg13 != null) {
      readEfDG13(_mrtdData!.dg13!.toBytes());
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG13',
          collapsedText: '',
          dataText: _mrtdData!.dg13!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg14 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG14',
          collapsedText: '',
          dataText: _mrtdData!.dg14!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.dg15 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG15',
          collapsedText: '',
          dataText: _mrtdData!.dg15!.toBytes().hex(),
        ),
      );
    }

    if (_mrtdData!.aaSig != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'Active Authentication signature',
          collapsedText: '',
          dataText: _mrtdData!.aaSig!.hex(),
        ),
      );
    }

    if (_mrtdData!.dg16 != null) {
      list.add(
        _makeMrtdDataWidget(
          header: 'EF.DG16',
          collapsedText: '',
          dataText: _mrtdData!.dg16!.toBytes().hex(),
        ),
      );
    }

    return list;
  }

  PlatformScaffold _buildPage(BuildContext context) => PlatformScaffold(
    appBar: PlatformAppBar(title: Text('MRTD Example App Quang Anh')),
    iosContentPadding: false,
    iosContentBottomPadding: false,
    body: Material(
      child: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(8.0),
          child: SingleChildScrollView(
            controller: _scrollController,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                _buildForm(context),
                SizedBox(height: 20),
                PlatformElevatedButton(
                  // btn Read MRTD
                  onPressed: _buttonPressed,
                  child: PlatformText(
                    _isReading ? 'Reading ...' : 'Read Passport',
                  ),
                ),
                SizedBox(height: 20),
                Row(
                  children: <Widget>[
                    Text(
                      'NFC available:',
                      style: TextStyle(
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(width: 4),
                    Text(
                      _isNfcAvailable ? "Yes" : "No",
                      style: TextStyle(fontSize: 18.0),
                    ),
                  ],
                ),
                SizedBox(height: 15),
                Text(
                  _alertMessage,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 15),
                Padding(
                  padding: EdgeInsets.all(8.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        _mrtdData != null ? "Passport Data:" : "",
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 15.0,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(
                          left: 16.0,
                          top: 8.0,
                          bottom: 8.0,
                        ),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: _mrtdDataWidgets(),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  );

  Widget _buildForm(BuildContext context) {
    return Column(
      children: <Widget>[
        TabBar(
          controller: _tabController,
          labelColor: Colors.blue,
          tabs: const <Widget>[Tab(text: 'DBA'), Tab(text: 'PACE')],
        ),
        Container(
          height: 400, // Increased height to accommodate the new scan button
          child: TabBarView(
            controller: _tabController,
            children: <Widget>[
              Card(
                borderOnForeground: false,
                elevation: 0,
                color: Colors.white,
                margin: const EdgeInsets.all(16.0),
                child: Form(
                  key: _mrzData,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      // Add a scan button at the top
                      Container(
                        width: double.infinity,
                        margin: const EdgeInsets.only(bottom: 16.0),
                        child: ElevatedButton.icon(
                          onPressed: _isReading ? null : _navigateToMrzScanner,
                          icon: Icon(Icons.document_scanner),
                          label: Text('Scan Passport MRZ'),
                          style: ElevatedButton.styleFrom(
                            padding: const EdgeInsets.symmetric(vertical: 12.0),
                          ),
                        ),
                      ),
                      TextFormField(
                        enabled: !_disabledInput(),
                        controller: _docNumber,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          labelText: 'Passport number',
                          fillColor: Colors.white,
                        ),
                        inputFormatters: <TextInputFormatter>[
                          FilteringTextInputFormatter.allow(
                            RegExp(r'[A-Z0-9]+'),
                          ),
                          LengthLimitingTextInputFormatter(14),
                        ],
                        textInputAction: TextInputAction.done,
                        textCapitalization: TextCapitalization.characters,
                        autofocus: true,
                        validator: (value) {
                          if (value?.isEmpty ?? false) {
                            return 'Please enter passport number';
                          }
                          return null;
                        },
                      ),
                      SizedBox(height: 12),
                      TextFormField(
                        enabled: !_disabledInput(),
                        controller: _dob,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          labelText: 'Date of Birth',
                          fillColor: Colors.white,
                        ),
                        autofocus: false,
                        validator: (value) {
                          if (value?.isEmpty ?? false) {
                            return 'Please select Date of Birth';
                          }
                          return null;
                        },
                        onTap: () async {
                          FocusScope.of(context).requestFocus(FocusNode());
                          // Can pick date which dates 15 years back or more
                          final now = DateTime.now();
                          final firstDate = DateTime(
                            now.year - 90,
                            now.month,
                            now.day,
                          );
                          final lastDate = DateTime(
                            now.year - 15,
                            now.month,
                            now.day,
                          );
                          final initDate = _getDOBDate();
                          final date = await _pickDate(
                            context,
                            firstDate,
                            initDate ?? lastDate,
                            lastDate,
                          );

                          FocusScope.of(context).requestFocus(FocusNode());
                          if (date != null) {
                            _dob.text = date;
                          }
                        },
                      ),
                      SizedBox(height: 12),
                      TextFormField(
                        enabled: !_disabledInput(),
                        controller: _doe,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          labelText: 'Date of Expiry',
                          fillColor: Colors.white,
                        ),
                        autofocus: false,
                        validator: (value) {
                          if (value?.isEmpty ?? false) {
                            return 'Please select Date of Expiry';
                          }
                          return null;
                        },
                        onTap: () async {
                          FocusScope.of(context).requestFocus(FocusNode());
                          // Can pick date from tomorrow and up to 10 years
                          final now = DateTime.now();
                          final firstDate = DateTime(
                            now.year,
                            now.month,
                            now.day + 1,
                          );
                          final lastDate = DateTime(
                            now.year + 10,
                            now.month + 6,
                            now.day,
                          );
                          final initDate = _getDOEDate();
                          final date = await _pickDate(
                            context,
                            firstDate,
                            initDate ?? firstDate,
                            lastDate,
                          );

                          FocusScope.of(context).requestFocus(FocusNode());
                          if (date != null) {
                            _doe.text = date;
                          }
                        },
                      ),
                      SizedBox(height: 12),
                      CheckboxListTile(
                        title: Text('DBA with PACE'),
                        value: _checkBoxPACE,
                        onChanged: (newValue) {
                          setState(() {
                            _checkBoxPACE = !_checkBoxPACE;
                          });
                        },
                      ),
                    ],
                  ),
                ),
              ),
              Card(
                borderOnForeground: false,
                elevation: 0,
                color: Colors.white,
                //shadowColor: Colors.white,
                margin: const EdgeInsets.all(16.0),
                child: Form(
                  key: _canData,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      TextFormField(
                        enabled: !_disabledInput(),
                        controller: _can,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          labelText: 'CAN number',
                          fillColor: Colors.white,
                        ),
                        inputFormatters: <TextInputFormatter>[
                          FilteringTextInputFormatter.allow(RegExp(r'[0-9]+')),
                          LengthLimitingTextInputFormatter(6),
                        ],
                        textInputAction: TextInputAction.done,
                        textCapitalization: TextCapitalization.characters,
                        autofocus: true,
                        validator: (value) {
                          if (value?.isEmpty ?? false) {
                            return 'Please enter CAN number';
                          }
                          return null;
                        },
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  void extractCertificatesFromSOD(Uint8List sodBytes) {
    print('➡️ SOD bytes length: ${sodBytes.length}');
    print('➡️ First 10 bytes: ${sodBytes.take(10).toList()}');

    final outerParser = ASN1Parser(sodBytes);
    final outerObject = outerParser.nextObject();

    // Bỏ lớp ngoài [APPLICATION 0]
    final innerParser = ASN1Parser(outerObject.valueBytes());
    final topLevel = innerParser.nextObject();

    if (topLevel is! ASN1Sequence) {
      print('❌ Inner content is not ASN1Sequence');
      return;
    }

    final topLevelSeq = topLevel;

    if (topLevelSeq.elements.length < 2) {
      print('❌ Dữ liệu SOD không đúng định dạng (không có đủ 2 phần tử)');
      return;
    }

    final oid = topLevelSeq.elements[0];
    print('📛 OID: ${(oid as ASN1ObjectIdentifier)}');

    final signedDataWrapper = topLevelSeq.elements[1];

    final signedDataParser = ASN1Parser(signedDataWrapper.valueBytes());
    final signedData = signedDataParser.nextObject();

    if (signedData is! ASN1Sequence) {
      print('❌ SignedData không phải ASN1Sequence');
      return;
    }

    final signedDataSeq = signedData;

    // 🔍 In chi tiết các phần tử trong SignedData để xác định cert nằm ở đâu
    print('📦 SignedData có ${signedDataSeq.elements.length} phần tử:');
    for (int i = 0; i < signedDataSeq.elements.length; i++) {
      final el = signedDataSeq.elements[i];
      print(
        '  🔹 Element $i: tag=${el.tag}, type=${el.runtimeType}, length=${el.encodedBytes.length}',
      );
    }

    // 👉 Gợi ý: thường certificate nằm ở element có tag == 0, bạn có thể cần thay đổi chỉ số
    // ASN1Object? certBlock;
    // for (int i = 0; i < signedDataSeq.elements.length; i++) {
    //   final el = signedDataSeq.elements[i];
    //   if (el.tag == 0 && el.valueBytes().isNotEmpty) {
    //     certBlock = el;
    //     print('✅ Found certificate block at element $i');
    //     break;
    //   }
    // }

    // final certBlock = signedDataSeq.elements[3];

    // if (certBlock == null) {
    //   print('❌ Không tìm thấy certificates trong SignedData');
    //   return;
    // }

    final certBlockRaw = signedDataSeq.elements[3];
    final certBlockParser = ASN1Parser(certBlockRaw.valueBytes());
    final certSet = certBlockParser.nextObject();

    print(
      '🔐 Tìm thấy tag=${certSet.tag} type=${certSet.runtimeType} length=${certSet.encodedBytes.length}',
    );

    if (certSet is! ASN1Set && certSet is! ASN1Sequence) {
      print('❌ Certificate block không phải ASN1Set hoặc ASN1Sequence');
      return;
    }

    List<ASN1Object> certList = [];

    if (certSet is ASN1Set) {
      certList = certSet.elements.toList();
    } else if (certSet is ASN1Sequence) {
      certList = certSet.elements.toList();
    } else {
      print('❌ Certificate block không phải ASN1Set hoặc ASN1Sequence');
      return;
    }

    // final certList = certSet.elements.toList();
    // print('🔐 Tìm thấy ${certList.length} certificate(s):');

    for (int i = 0; i < certList.length; i++) {
      final cert = certList[i];
      final certBytes = cert.encodedBytes;
      final base64Cert = base64.encode(certBytes);

      print('\n📄 Certificate $i (length: ${certBytes.length} bytes)');
      print('-----BEGIN CERTIFICATE-----');
      print(base64Cert);
      print('-----END CERTIFICATE-----');

      var certString = base64Cert.replaceAll('\n', '');
      print('certString: $certString');

      // Convert base64 to PEM format
      final pemCert =
          '-----BEGIN CERTIFICATE-----\n$certString\n-----END CERTIFICATE-----';
      print('PEM certificate: $pemCert');
    }

    print(
      '========================================================================',
    );
  }

  void readEfDG13(Uint8List efDG13Bytes) {
    print('📦 EF.DG13 length: ${efDG13Bytes.length}');
    print('➡️ First 10 bytes: ${efDG13Bytes.take(10).toList()}');

    try {
      final parser = ASN1Parser(efDG13Bytes);
      final topLevel = parser.nextObject();

      if (topLevel is! ASN1Sequence) {
        print('❌ EF.DG13 không phải ASN1Sequence');
        return;
      }

      final seq = topLevel;
      print('📚 EF.DG13 gồm ${seq.elements.length} phần tử:');

      for (int i = 0; i < seq.elements.length; i++) {
        final el = seq.elements[i];
        print(
          '  🔹 Element $i: tag=${el.tag}, type=${el.runtimeType}, length=${el.encodedBytes.length}',
        );

        // Nếu phần tử là 1 SEQUENCE lớn, ta phân tích tiếp
        if (el is ASN1Sequence) {
          print('🔎 Phân tích sâu Element $i:');

          final innerElements = el.elements.toList();
          for (int j = 0; j < innerElements.length; j++) {
            final subEl = innerElements[j];
            print(
              '    🔸 Sub-element $j: tag=${subEl.tag}, type=${subEl.runtimeType}, length=${subEl.encodedBytes.length}',
            );

            // Nếu là ASN1Set chứa các SEQUENCE(tag, value), ta parse tiếp
            if (subEl is ASN1Set) {
              final subItems = subEl.elements.toList();
              for (int k = 0; k < subItems.length; k++) {
                final item = subItems[k];
                if (item is ASN1Sequence && item.elements.length == 2) {
                  final tag = item.elements[0];
                  final value = item.elements[1];

                  final tagNumber = (tag is ASN1Integer) ? tag.intValue : null;
                  final valueStr = _tryDecodeString(value.valueBytes());

                  print('       🏷️ Field $tagNumber: $valueStr');
                }
              }
            }
          }
        }
      }

      print('✅ Hoàn tất đọc EF.DG13');
    } catch (e) {
      print('❌ Lỗi khi phân tích EF.DG13: $e');
    }
  }

  String? _tryDecodeString(Uint8List bytes) {
    try {
      return utf8.decode(bytes);
    } catch (_) {
      return null;
    }
  }

  // String getDsCertFileEncoded(EfSOD sodFile) {
  //   try {
  //     // Get PEM certificate
  //     final String pemCertificate = sodFile.toPEM();

  //     // Encode PEM to base64
  //     return base64Encode(utf8.encode(pemCertificate));
  //   } catch (e) {
  //     print('Error getting DS certificate: $e');
  //     return "";
  //   }
  // }

  // String convertToPEMFormat(Uint8List certificateBytes) {
  //   final StringBuffer pemFormat = StringBuffer();
  //   pemFormat.write("-----BEGIN CERTIFICATE-----\n");

  //   // Convert DER to base64 and format to 64 characters per line
  //   final String base64Cert = base64Encode(certificateBytes);
  //   for (int i = 0; i < base64Cert.length; i += 64) {
  //     final int end = (i + 64 < base64Cert.length) ? i + 64 : base64Cert.length;
  //     pemFormat.write(base64Cert.substring(i, end));
  //     pemFormat.write("\n");
  //   }

  //   pemFormat.write("-----END CERTIFICATE-----\n");
  //   return pemFormat.toString();
  // }

  void _navigateToMrzScanner() async {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder:
            (context) => MrzScannerScreen(
              onMrzDataReceived: (docNumber, dob, doe) {
                setState(() {
                  _docNumber.text = docNumber;
                  _dob.text = dob;
                  _doe.text = doe;
                });
              },
            ),
      ),
    );
  }
}