flutter_cccd_vn — ICAO 9303 MRTD library with a Vietnamese CCCD (căn cước công dân) NFC adapter

LGPL v3 tests

🇻🇳 Đọc bằng tiếng Việt

flutter_cccd_vn is a fork of ZeroPass/dmrtd, a Dart implementation of the ICAO 9303 standard for biometric passports (MRTD), with a Vietnamese CCCD (căn cước công dân) NFC reading adapter built on top of the same session/secure messaging layer.

Key features

Core ICAO 9303 / MRTD

  • PACE & BAC session key establishment protocol
  • Reading all elementary files from MRTD, e.g.: EF.DG1, EF.DG2, EF.DG11, EF.DG12, EF.DG15 ... (most files are not fully parsed yet)
  • Executing Active Authentication on MRTD (i.e. sign arbitrary data with the passport)
  • Basic implementation of ICC ISO7816-4 smart card standard
  • Implementation of ISO 9797 Algorithm 3 MAC and padding scheme

CCCD Việt Nam adapter

  • CccdReader: typed, retrying reader with classified errors (NFC disabled, timeout, tag lost, cancellation, connection, authentication, parse, integrity, busy)
  • CccdCan (PACE via the 6-digit CAN) and CccdMrz (BAC via document number + date of birth + date of expiry — no CAN needed when those three fields are available, e.g. from OCR)
  • CccdReadProgress callback reporting real per-EF progress (document number → EF.COM → portrait → …), not a fake animation
  • CccdOcrParser: Vietnamese-label OCR parsing for both the 2021 "CCCD gắn chip" and the 2024 "Căn cước" card layouts, with a checksum-validated MRZ fast path
  • CccdVerificationResult / CccdFaceMatchResult: pure-Dart contracts to compare NFC chip data, OCR text and an on-device face match

Library structure

dmrtd.dart — public passport (MRTD) API
cccd_vietnam.dart — public CCCD Việt Nam API (CccdReader, CccdCan, CccdMrz, CccdOcrParser, CccdVerificationResult, ...)
extensions.dart — exposes the library's Dart extensions
internal.dart — exposes internal components such as MrtdApi, ICC and crypto

Installation

dependencies:
  flutter_cccd_vn: ^2.0.0

or, from a local checkout:

dependencies:
  flutter_cccd_vn:
    path: '<path_to_flutter_cccd_vn_folder>'

Then:

flutter pub get

Quick start — CCCD Việt Nam

Note: see also the example app for a full capture → OCR → NFC scan flow.

import 'package:flutter_cccd_vn/cccd_vietnam.dart';

final nfc = NfcProvider();
final reader = CccdReader(
  transport: nfc,
  protocol: IcaoCccdProtocol(),
  operationTimeout: const Duration(seconds: 30),
  maxRetries: 1,
);

await reader.connect();

// BAC via MRZ: no CAN required. documentNumber is the chip's ICAO
// document number field — the 9 digits of the 12-digit số định danh
// *without* its 3-digit province-code prefix.
final credential = CccdMrz(
  documentNumber: '099888777',
  dateOfBirth: DateTime(1995, 3, 15),
  dateOfExpiry: DateTime(2038, 5, 15),
);

final data = await reader.read(
  credential,
  onProgress: (completed, total, stage) => print('$stage ($completed/$total)'),
);
await reader.disconnect();

print(data.documentNumber);
print(data.fullName);

To open the chip with the 6-digit CAN (PACE) instead of MRZ/BAC:

final data = await reader.read(CccdCan('123456'));

For a faster identity-only read, skip the optional files:

protocol: IcaoCccdProtocol(
  includePortrait: false,
  includeDg13: false,
),

Notes

  • The adapter reads DG1 and optionally DG2/DG13 according to EF.COM. EAC/DG3/DG4 and full real-card compatibility still require physical-card validation and terminal credentials.
  • Do not log CAN values, MRZ values or card payloads.
  • Retries apply only to completed transient connection failures; a timeout cannot be safely retried because Dart cannot cancel an in-flight NFC Future. After a timeout the reader is poisoned: call disconnect() for cleanup and create a new CccdReader before retrying. Tag-lost and user-cancelled operations require a new user interaction.
  • The chip's MRZ document number is ICAO's 9-digit short form; when comparing it against a 12-digit số định danh read via OCR, compare the overlapping 9-digit suffix (CccdVerificationResult.documentNumberMatches already does this for you).

NFC platform setup

Android: the example already declares the NFC permission and hardware support in example/android/app/src/main/AndroidManifest.xml.

iOS: enable the Near Field Communication Tag Reader Session Formats capability, add NFCReaderUsageDescription to Info.plist, and add the ISO7816 reader entitlement required by the app's provisioning profile. This repository does not contain an example/ios target.

Local CCCD capture, OCR and face verification (example app)

The example app uses camera, ML Kit text recognition, ML Kit face detection and face_verification 0.3.8 to keep capture, OCR and FaceNet comparison on-device. The root package only exports the pure Dart contracts: CccdOcrParser, CccdFaceMatchResult and CccdVerificationResult.

The local face score is a similarity signal, not liveness detection or legal identity proof. CccdVerificationResult reports consistency, not identity verification — calibrate the threshold with real devices and lighting before production. The app deletes temporary images and the temporary face template after each attempt and does not log OCR, portrait, selfie or embeddings. face_verification may emit its own internal debug score/ID logs; production deployments requiring a strict biometric log policy should use a fork/configuration with those logs disabled.

The example flow is Android-only (no example/ios host target). An iOS consuming app must add NSCameraUsageDescription, NFC ISO7816 entitlements and use iOS 15.5+ for the selected face stack. DG2 JPEG2000 is reported as invalidPortrait; only JPEG is passed to the matcher.

Run on a real Android device:

cd example
flutter pub get
flutter run

Open Xác minh CCCD gắn chip, capture both card sides, review the OCR result, tap Bắt đầu quét NFC, then optionally capture a selfie for face matching. The result is matched, rejected or needsReview — not liveness or legal identity verification.

Quick start — Passport (MRTD)

import 'package:flutter_cccd_vn/dmrtd.dart';

final nfc = NfcProvider();
try {
  await nfc.connect(iosAlertMessage: 'Hold your phone near the passport');

  final passport = Passport(nfc);
  final cardAccess = await passport.readEfCardAccess();

  // PACE (recommended when EF.CardAccess is present) or BAC:
  final accessKey = DBAKey('L898902C3', DateTime(1974, 8, 12), DateTime(2012, 4, 15));
  if (cardAccess.isPaceInfoSet) {
    await passport.startSessionPACE(accessKey, cardAccess);
  } else {
    await passport.startSession(accessKey);
  }

  final com = await passport.readEfCOM();
  if (com.dgTags.contains(EfDG1.TAG)) {
    final dg1 = await passport.readEfDG1();
    print(dg1.mrz.documentNumber);
  }
  if (com.dgTags.contains(EfDG2.TAG)) {
    final dg2 = await passport.readEfDG2();
  }

  await nfc.disconnect();
} on PassportError catch (e) {
  await nfc.disconnect(iosErrorMessage: e.message);
}

Other documentation

License

This project is dual-licensed: the GNU Lesser General Public License (LGPL-3.0) for open-source use, and a Commercial License for proprietary use. See LICENSE (or LICENSE.LGPL) and LICENSE.COMMERCIAL for details.

Original library ("dmrtd") © 2022 ZeroPass. This fork adds the CCCD Việt Nam adapter under the same LGPL-3.0 terms.