SimulatedIcaoChip constructor

SimulatedIcaoChip({
  1. IcaoDocumentType type = IcaoDocumentType.passport,
  2. String? documentNumber,
  3. String issuingState = 'UTO',
  4. String nationality = 'UTO',
  5. String lastName = 'SPECIMEN',
  6. String firstNames = 'ALICE MARIE',
  7. PartialDate? birthDate,
  8. Sex sex = Sex.female,
  9. DateTime? expiryDate,
  10. String? birthPlace = 'ZENITH<UTOPIA',
  11. String? address = 'AVENUE DES NATIONS 1<1000 ZENITH<UTOPIA',
  12. String? issuingAuthority = 'MINISTRY OF FOREIGN AFFAIRS',
  13. DateTime? issueDate,
  14. String? personalNumber,
  15. Uint8List? photo,
  16. IcaoFaceEncoding faceEncoding = IcaoFaceEncoding.iso19794,
  17. String can = '123456',
  18. SimulatedChipAccess access = SimulatedChipAccess.paceAndBac,
  19. SimulatedChipProof proof = SimulatedChipProof.chipAuthentication,
  20. bool cloned = false,
  21. IcaoDataGroup? tamperedGroup,
  22. Duration latency = Duration.zero,
})

A chip holding the given data, by default that of a fictional citizen.

Names are given as in the MRZ, upper case. birthPlace and address go into DG11, issuingAuthority and issueDate into DG12. A personalNumber goes into the optional data of the MRZ, as Belgium puts the national number there, and into DG11.

tamperedGroup is changed after signing, so Passive Authentication fails. A cloned chip fails its proof. latency delays every answer.

Implementation

SimulatedIcaoChip({
  this.type = IcaoDocumentType.passport,
  String? documentNumber,
  String issuingState = 'UTO',
  String nationality = 'UTO',
  String lastName = 'SPECIMEN',
  String firstNames = 'ALICE MARIE',
  PartialDate? birthDate,
  Sex sex = Sex.female,
  DateTime? expiryDate,
  String? birthPlace = 'ZENITH<UTOPIA',
  String? address = 'AVENUE DES NATIONS 1<1000 ZENITH<UTOPIA',
  String? issuingAuthority = 'MINISTRY OF FOREIGN AFFAIRS',
  DateTime? issueDate,
  String? personalNumber,
  Uint8List? photo,
  this.faceEncoding = IcaoFaceEncoding.iso19794,
  this.can = '123456',
  this.access = SimulatedChipAccess.paceAndBac,
  this.proof = SimulatedChipProof.chipAuthentication,
  this.cloned = false,
  IcaoDataGroup? tamperedGroup,
  this.latency = Duration.zero,
})  : documentNumber = documentNumber ??
          (type == IcaoDocumentType.passport ? 'UT1234567' : 'UTD123456'),
      birthDate = birthDate ?? PartialDate(1990, 5, 15),
      expiryDate = expiryDate ?? DateTime.utc(2034, 3, 13) {
  final mrz = _mrz(
    type: type,
    number: this.documentNumber,
    state: issuingState,
    nationality: nationality,
    lastName: lastName,
    firstNames: firstNames,
    birthDate: this.birthDate,
    sex: sex,
    expiryDate: this.expiryDate,
    personalNumber: personalNumber ?? '',
  );
  final face = photo ?? specimenPhoto;
  final groups = <IcaoDataGroup, Uint8List>{
    IcaoDataGroup.dg1: tlv(0x61, tlv(0x5F1F, ascii.encode(mrz))),
    IcaoDataGroup.dg2: _dg2(face, faceEncoding, sex),
    IcaoDataGroup.dg11: _dg11(
      name: '${lastName.replaceAll(' ', '<')}<<'
          '${firstNames.replaceAll(' ', '<')}',
      birthDate: this.birthDate,
      birthPlace: birthPlace,
      address: address,
      personalNumber: personalNumber,
    ),
    IcaoDataGroup.dg12: _dg12(
      issuingAuthority,
      issueDate ?? DateTime.utc(2024, 3, 14),
    ),
  };

  final dg14 = <Uint8List>[];
  if (access != SimulatedChipAccess.bacOnly) {
    dg14.add(_paceInfo(_paceOid));
  }
  switch (proof) {
    case SimulatedChipProof.chipAuthentication:
      dg14
        ..add(_chipAuthenticationInfo)
        ..add(_chipAuthenticationKeyInfo);
    case SimulatedChipProof.activeAuthenticationEc:
      dg14.add(derSequence([
        derObjectIdentifier(activeAuthenticationOid),
        derInteger(BigInt.one),
        derObjectIdentifier('0.4.0.127.0.7.1.1.4.1.3'),
      ]));
      groups[IcaoDataGroup.dg15] = tlv(
        0x6F,
        SimulatedPki.subjectPublicKeyInfo(
          SimulatedPki.publicKeyOf(_activeEcKey),
        ),
      );
    case SimulatedChipProof.activeAuthenticationRsa:
      groups[IcaoDataGroup.dg15] =
          tlv(0x6F, rsaSubjectPublicKeyInfo(_rsaKey));
    case SimulatedChipProof.chipAuthenticationMapping:
    case SimulatedChipProof.none:
  }
  if (dg14.isNotEmpty) groups[IcaoDataGroup.dg14] = tlv(0x6E, derSet(dg14));

  final pki = SimulatedPki.instance;
  final sorted = groups.keys.toList()
    ..sort((a, b) => a.number.compareTo(b.number));
  final lds = derSequence([
    derInteger(BigInt.zero),
    derSequence([
      derObjectIdentifier(HashAlgorithm.sha256.objectIdentifier),
    ]),
    derSequence([
      for (final group in sorted)
        derSequence([
          derInteger(BigInt.from(group.number)),
          derOctetString(HashAlgorithm.sha256.digest(groups[group]!)),
        ]),
    ]),
  ]);
  final sod = tlv(0x77, pki.signedData('2.23.136.1.1.1', lds));

  if (tamperedGroup != null && groups[tamperedGroup] != null) {
    final file = Uint8List.fromList(groups[tamperedGroup]!);
    file[file.length - 1] ^= 0x01;
    groups[tamperedGroup] = file;
  }

  _application = {
    0x011E: tlv(
      0x60,
      concat([
        tlv(0x5F01, ascii.encode('0107')),
        tlv(0x5F36, ascii.encode('040000')),
        tlv(0x5C, [for (final group in sorted) group.tag]),
      ]),
    ),
    0x011D: sod,
    for (final group in sorted) group.fileId: groups[group]!,
  };
  _master = {
    if (access != SimulatedChipAccess.bacOnly)
      0x011C: derSet([_paceInfo(_paceOid)]),
    if (proof == SimulatedChipProof.chipAuthenticationMapping)
      0x011D: pki.signedData(
        '0.4.0.127.0.7.3.2.1',
        derSet([
          _paceInfo(_paceOid),
          _chipAuthenticationInfo,
          _chipAuthenticationKeyInfo,
        ]),
      ),
  };
  _mrzKey = IcaoMrzKey(
    documentNumber: this.documentNumber,
    birthDate: this.birthDate,
    expiryDate: this.expiryDate,
  );
}