From 5feef53598428a0f56142ebd7d12bf9b24e57256 Mon Sep 17 00:00:00 2001 From: Abdullah Karacabey Date: Tue, 4 Oct 2022 13:51:42 +0300 Subject: [PATCH 1/4] Add partial parsing of DG2, DG11 and DG12 && Refactor `MRZ` class * dg2 photo data added * dg11 added * dg12 issuing authority and date of issue parsed * parse date method added * photo type added * photo type added * date time parse bug fix solved * raw content added * TAG_LIST_TAG as constant added * unused constants removed * removed unnecessary comment * update for reviews * toBytes and toEncodedString methods added * version number added as a const * not parsed props removed * not parsed props removed. * added BIOMETRIC_INFORMATION_COUNT_TAG * reformat assignments --- lib/src/extension/string_apis.dart | 15 +- lib/src/lds/df1/efdg11.dart | 126 +++++++++++++++- lib/src/lds/df1/efdg12.dart | 71 ++++++++- lib/src/lds/df1/efdg2.dart | 223 +++++++++++++++++++++++++++++ lib/src/lds/mrz.dart | 131 +++++++++-------- 5 files changed, 493 insertions(+), 73 deletions(-) diff --git a/lib/src/extension/string_apis.dart b/lib/src/extension/string_apis.dart index 696eabe..8f573da 100644 --- a/lib/src/extension/string_apis.dart +++ b/lib/src/extension/string_apis.dart @@ -16,7 +16,7 @@ extension StringDecodeApis on String { extension StringYYMMDDateApi on String { DateTime parseDateYYMMDD() { - if(length < 6) { + if (length < 6) { throw FormatException("invalid length of compact date string"); } @@ -27,10 +27,17 @@ extension StringYYMMDDateApi on String { // Sub 100 years from parsed year if greater than 10 years and 5 months from now. final now = DateTime.now(); final tenYearsFromNow = now.year + 10; - if (y > tenYearsFromNow || - (y == tenYearsFromNow && now.month + 5 < m)) { + if (y > tenYearsFromNow || (y == tenYearsFromNow && now.month + 5 < m)) { y -= 100; } return DateTime(y, m, d); } -} \ No newline at end of file + + DateTime parseDate() { + if (length == 6) { + return this.parseDateYYMMDD(); + } else { + return DateTime.parse(this); + } + } +} diff --git a/lib/src/lds/df1/efdg11.dart b/lib/src/lds/df1/efdg11.dart index f7180ef..705ecd5 100644 --- a/lib/src/lds/df1/efdg11.dart +++ b/lib/src/lds/df1/efdg11.dart @@ -1,7 +1,12 @@ // Created by Crt Vavros, copyright © 2022 ZeroPass. All rights reserved. // ignore_for_file: constant_identifier_names +import 'dart:convert'; +import 'dart:core'; import 'dart:typed_data'; +import 'package:dmrtd/dmrtd.dart'; +import 'package:dmrtd/extensions.dart'; + import 'dg.dart'; class EfDG11 extends DataGroup { @@ -9,6 +14,60 @@ class EfDG11 extends DataGroup { static const SFI = 0x0B; static const TAG = DgTag(0x6B); + static const FULL_NAME_TAG = 0x5F0E; + static const OTHER_NAME_TAG = 0x5F0F; + static const PERSONAL_NUMBER_TAG = 0x5F10; + + // In 'CCYYMMDD' format. + static const FULL_DATE_OF_BIRTH_TAG = 0x5F2B; + + // Fields separated by '<' + static const PLACE_OF_BIRTH_TAG = 0x5F11; + + // Fields separated by '<' + static const PERMANENT_ADDRESS_TAG = 0x5F42; + static const TELEPHONE_TAG = 0x5F12; + static const PROFESSION_TAG = 0x5F13; + static const TITLE_TAG = 0x5F14; + static const PERSONAL_SUMMARY_TAG = 0x5F15; + + // Compressed image per ISO/IEC 10918 + static const PROOF_OF_CITIZENSHIP_TAG = 0x5F16; + + // Separated by '<' + static const OTHER_VALID_TD_NUMBERS_TAG = 0x5F17; + static const CUSTODY_INFORMATION_TAG = 0x5F18; + + static const TAG_LIST_TAG = 0x5c; + + String? _nameOfHolder; + final _otherNames = []; + String? _personalNumber; + DateTime? _fullDateOfBirth; + final _placeOfBirth = []; + final _permanentAddress = []; + String? _telephone; + String? _profession; + String? _title; + String? _personalSummary; + Uint8List? _proofOfCitizenship; + var _otherValidTDNumbers = []; + String? _custodyInformation; + + String? get nameOfHolder => _nameOfHolder; + List get otherNames => _otherNames; + String? get personalNumber => _personalNumber; + DateTime? get fullDateOfBirth => _fullDateOfBirth; + List get placeOfBirth => _placeOfBirth; + List get permanentAddress => _permanentAddress; + String? get telephone => _telephone; + String? get profession => _profession; + String? get title => _title; + String? get ersonalSummary => _personalSummary; + Uint8List? get proofOfCitizenship => _proofOfCitizenship; + List get otherValidTDNumbers => _otherValidTDNumbers; + String? get custodyInformation => _custodyInformation; + EfDG11.fromBytes(Uint8List data) : super.fromBytes(data); @override @@ -19,4 +78,69 @@ class EfDG11 extends DataGroup { @override int get tag => TAG.value; -} \ No newline at end of file + + @override + void parse(Uint8List content) { + final tlv = TLV.fromBytes(content); + if (tlv.tag != tag) { + throw EfParseError( + "Invalid DG11 tag=${tlv.tag.hex()}, expected tag=${TAG.value.hex()}"); + } + + final data = tlv.value; + final tagListTag = TLV.decode(data); + if (tagListTag.tag.value != TAG_LIST_TAG) { + throw EfParseError( + "Invalid version object tag=${tagListTag.tag.value.hex()}, expected version object with tag=5c"); + } + var tagListLength = tlv.value.length; + int tagListBytesRead = tagListTag.encodedLen; + + while (tagListBytesRead < tagListLength) { + final uvtv = TLV.decode(data.sublist(tagListBytesRead)); + tagListBytesRead += uvtv.encodedLen; + + switch (uvtv.tag.value) { + case FULL_NAME_TAG: + _nameOfHolder = utf8.decode(uvtv.value); + break; + case PERSONAL_NUMBER_TAG: + _personalNumber = utf8.decode(uvtv.value); + break; + case OTHER_NAME_TAG: + _otherNames.add(utf8.decode(uvtv.value)); + break; + case FULL_DATE_OF_BIRTH_TAG: + _fullDateOfBirth = String.fromCharCodes(uvtv.value).parseDate(); + break; + case PLACE_OF_BIRTH_TAG: + _placeOfBirth.add(utf8.decode(uvtv.value)); + break; + case PERMANENT_ADDRESS_TAG: + _permanentAddress.add(utf8.decode(uvtv.value)); + break; + case TELEPHONE_TAG: + _telephone = utf8.decode(uvtv.value); + break; + case PROFESSION_TAG: + _profession = utf8.decode(uvtv.value); + break; + case TITLE_TAG: + _title = utf8.decode(uvtv.value); + break; + case PERSONAL_SUMMARY_TAG: + _personalSummary = utf8.decode(uvtv.value); + break; + case PROOF_OF_CITIZENSHIP_TAG: + _proofOfCitizenship = uvtv.value; + break; + case OTHER_VALID_TD_NUMBERS_TAG: + _otherValidTDNumbers.add(utf8.decode(uvtv.value)); + break; + case CUSTODY_INFORMATION_TAG: + _custodyInformation = utf8.decode(uvtv.value); + break; + } + } + } +} diff --git a/lib/src/lds/df1/efdg12.dart b/lib/src/lds/df1/efdg12.dart index deabd1a..7217a24 100644 --- a/lib/src/lds/df1/efdg12.dart +++ b/lib/src/lds/df1/efdg12.dart @@ -1,14 +1,49 @@ // Created by Crt Vavros, copyright © 2022 ZeroPass. All rights reserved. // ignore_for_file: constant_identifier_names +import 'dart:core'; +import 'dart:convert'; import 'dart:typed_data'; +import 'package:dmrtd/extensions.dart'; + import 'dg.dart'; +import '../ef.dart'; +import '../tlv.dart'; class EfDG12 extends DataGroup { static const FID = 0x010C; static const SFI = 0x0C; static const TAG = DgTag(0x6C); + static const ISSUING_AUTHORITY_TAG = 0x5F19; + + // yyyymmdd + static const DATE_OF_ISSUE_TAG = 0x5F26; + + // formatted per ICAO 9303 rules + static const NAME_OF_OTHER_PERSON_TAG = 0x5F1A; + static const ENDORSEMENTS_AND_OBSERVATIONS_TAG = 0x5F1B; + static const TAX_OR_EXIT_REQUIREMENTS_TAG = 0x5F1C; + + // Image per ISO/IEC 10918 + static const IMAGE_OF_FRONT_TAG = 0x5F1D; + + // Image per ISO/IEC 10918 + static const IMAGE_OF_REAR_TAG = 0x5F1E; + + // yyyymmddhhmmss + static const DATE_AND_TIME_OF_PERSONALIZATION = 0x5F55; + static const PERSONALIZATION_SYSTEM_SERIAL_NUMBER_TAG = 0x5F56; + + static const TAG_LIST_TAG = 0x5c; + + DateTime? _dateOfIssue; + String? _issuingAuthority; + + DateTime? get dateOfIssue => _dateOfIssue; + String? get issuingAuthority => _issuingAuthority; + + EfDG12.fromBytes(Uint8List data) : super.fromBytes(data); @override @@ -19,4 +54,38 @@ class EfDG12 extends DataGroup { @override int get tag => TAG.value; -} \ No newline at end of file + + @override + void parse(Uint8List content) { + final tlv = TLV.fromBytes(content); + if (tlv.tag != tag) { + throw EfParseError( + "Invalid DG12 tag=${tlv.tag.hex()}, expected tag=${TAG.value.hex()}"); + } + + final data = tlv.value; + final tagListTag = TLV.decode(data); + if (tagListTag.tag.value != TAG_LIST_TAG) { + throw EfParseError( + "Invalid version object tag=${tagListTag.tag.value.hex()}, expected version object with tag=5c"); + } + var tagListLength = tlv.value.length; + int tagListBytesRead = tagListTag.encodedLen; + + // int expectedTagCount = (tagListLength / 2).toInt(); + + while (tagListBytesRead < tagListLength) { + final uvtv = TLV.decode(data.sublist(tagListBytesRead)); + tagListBytesRead += uvtv.encodedLen; + + switch (uvtv.tag.value) { + case ISSUING_AUTHORITY_TAG: + _issuingAuthority = utf8.decode(uvtv.value); + break; + case DATE_OF_ISSUE_TAG: + _dateOfIssue = String.fromCharCodes(uvtv.value).parseDate(); + break; + } + } + } +} diff --git a/lib/src/lds/df1/efdg2.dart b/lib/src/lds/df1/efdg2.dart index 057c2ef..59b1058 100644 --- a/lib/src/lds/df1/efdg2.dart +++ b/lib/src/lds/df1/efdg2.dart @@ -1,14 +1,33 @@ // Created by Crt Vavros, copyright © 2022 ZeroPass. All rights reserved. // ignore_for_file: constant_identifier_names +import 'dart:core'; +import 'dart:convert'; import 'dart:typed_data'; +import 'package:dmrtd/dmrtd.dart'; +import 'package:dmrtd/extensions.dart'; + import 'dg.dart'; +enum ImageType { jpeg, jpeg2000 } + class EfDG2 extends DataGroup { static const FID = 0x0102; static const SFI = 0x02; static const TAG = DgTag(0x75); + static const BIOMETRIC_INFORMATION_GROUP_TEMPLATE_TAG = 0x7F61; + static const BIOMETRIC_INFORMATION_TEMPLATE_TAG = 0x7F60; + + static const BIOMETRIC_HEADER_TEMPLATE_BASE_TAG = 0xA1; + + static const BIOMETRIC_DATA_BLOCK_TAG = 0x5F2E; + static const BIOMETRIC_DATA_BLOCK_CONSTRUCTED_TAG = 0x7F2E; + + static const BIOMETRIC_INFORMATION_COUNT_TAG = 0x02; + static const SMT_TAG = 0x7D; + static const VERSION_NUMBER = 0x30313000; + EfDG2.fromBytes(Uint8List data) : super.fromBytes(data); @override @@ -19,4 +38,208 @@ class EfDG2 extends DataGroup { @override int get tag => TAG.value; + + late int versionNumber; + late int lengthOfRecord; + late int numberOfFacialImages; + late int facialRecordDataLength; + late int nrFeaturePoints; + late int gender; + late int eyeColor; + late int hairColor; + late int featureMask; + late int expression; + late int poseAngle; + late int poseAngleUncertainty; + late int faceImageType; + late int imageWidth; + late int imageHeight; + late int imageColorSpace; + late int sourceType; + late int deviceType; + late int quality; + + Uint8List? imageData; + int? _imageDataType; + + ImageType? get imageType { + if (_imageDataType == null) return null; + + return _imageDataType == 0 ? ImageType.jpeg : ImageType.jpeg2000; + } + + @override + void parse(Uint8List content) { + final tlv = TLV.fromBytes(content); + if (tlv.tag != tag) { + throw EfParseError( + "Invalid DG2 tag=${tlv.tag.hex()}, expected tag=${TAG.value.hex()}"); + } + + final data = tlv.value; + final bigt = TLV.decode(data); + + if (bigt.tag.value != BIOMETRIC_INFORMATION_GROUP_TEMPLATE_TAG) { + throw EfParseError( + "Invalid object tag=${bigt.tag.value.hex()}, expected tag=$BIOMETRIC_INFORMATION_GROUP_TEMPLATE_TAG"); + } + + final bict = TLV.decode(bigt.value); + + if (bict.tag.value != BIOMETRIC_INFORMATION_COUNT_TAG) { + throw EfParseError( + "Invalid object tag=${bict.tag.value.hex()}, expected tag=$BIOMETRIC_INFORMATION_COUNT_TAG"); + } + + int bitCount = (bict.value[0] & 0xFF); + + for (var i = 0; i < bitCount; i++) { + _readBIT(bigt.value.sublist(bict.encodedLen), i); + } + } + + _readBIT(Uint8List stream, int index) { + final tvl = TLV.decode(stream); + + if (tvl.tag.value != BIOMETRIC_INFORMATION_TEMPLATE_TAG) { + throw EfParseError( + "Invalid object tag=${tvl.tag.value.hex()}, expected tag=${BIOMETRIC_INFORMATION_TEMPLATE_TAG}"); + } + + var bht = TLV.decode(tvl.value); + + if (bht.tag.value == SMT_TAG) { + _readStaticallyProtectedBIT(); + } else if ((bht.tag.value & 0xA0) == 0xA0) { + var sbh = _readBHT(tvl.value); + + _readBiometricDataBlock(sbh); + } + } + + //TODO Reads a biometric information template protected with secure messaging. + _readStaticallyProtectedBIT() {} + + List _readBHT(Uint8List stream) { + final bht = TLV.decode(stream); + + if (bht.tag.value != BIOMETRIC_HEADER_TEMPLATE_BASE_TAG) { + throw EfParseError( + "Invalid object tag=${bht.tag.value.hex()}, expected tag=${BIOMETRIC_INFORMATION_TEMPLATE_TAG}"); + } + + int bhtLength = stream.length; + int bytesRead = bht.encodedLen; + var elements = []; + while (bytesRead < bhtLength) { + final tlv = TLV.decode(stream.sublist(bytesRead)); + bytesRead += tlv.encodedLen; + elements.add(tlv); + } + + return elements; + } + + _readBiometricDataBlock(List sbh) { + var firstBlock = sbh.first; + if (firstBlock.tag.value != BIOMETRIC_DATA_BLOCK_TAG && + firstBlock.tag.value != BIOMETRIC_DATA_BLOCK_CONSTRUCTED_TAG) { + throw EfParseError( + "Invalid object tag=${firstBlock.tag.value.hex()}, expected tag=$BIOMETRIC_DATA_BLOCK_TAG or $BIOMETRIC_DATA_BLOCK_CONSTRUCTED_TAG "); + } + + var data = firstBlock.value; + if (data[0] != 0x46 && + data[1] != 0x41 && + data[2] != 0x43 && + data[3] != 0x00) { + throw EfParseError("Biometric data block is invalid"); + } + + var offset = 4; + + versionNumber = _extractContent(data, start: offset, end: offset + 4); + + if (versionNumber != VERSION_NUMBER) { + throw EfParseError("Version of Biometric data is not valid"); + } + + offset += 4; + + lengthOfRecord = _extractContent(data, start: offset, end: offset + 4); + offset += 4; + + numberOfFacialImages = + _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + facialRecordDataLength = + _extractContent(data, start: offset, end: offset + 4); + offset += 4; + + nrFeaturePoints = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + gender = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + eyeColor = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + hairColor = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + featureMask = _extractContent(data, start: offset, end: offset + 3); + offset += 3; + + expression = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + poseAngle = _extractContent(data, start: offset, end: offset + 3); + offset += 3; + + poseAngleUncertainty = + _extractContent(data, start: offset, end: offset + 3); + offset += 3; + + // Features (not handled). There shouldn't be any but if for some reason there were, + // then we are going to skip over them + // The Feature block is 8 bytes + offset += nrFeaturePoints * 8; + + faceImageType = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + _imageDataType = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + imageWidth = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + imageHeight = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + imageColorSpace = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + sourceType = _extractContent(data, start: offset, end: offset + 1); + offset += 1; + + deviceType = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + quality = _extractContent(data, start: offset, end: offset + 2); + offset += 2; + + imageData = sbh.first.value.sublist(offset); + } + + int _extractContent(Uint8List data, {required int start, required int end}) { + if (end - start == 1) { + return data.sublist(start, end).buffer.asByteData().getInt8(0); + } else if (end - start < 4) + return data.sublist(start, end).buffer.asByteData().getInt16(0); + // else if(end - start == 4) + return data.sublist(start, end).buffer.asByteData().getInt32(0); + } } diff --git a/lib/src/lds/mrz.dart b/lib/src/lds/mrz.dart index 9220cfc..f9f7634 100644 --- a/lib/src/lds/mrz.dart +++ b/lib/src/lds/mrz.dart @@ -7,6 +7,7 @@ import '../extension/datetime_apis.dart'; import '../extension/string_apis.dart'; enum MRZVersion { td1, td2, td3 } + class MRZParseError implements Exception { final String message; MRZParseError(this.message); @@ -31,11 +32,25 @@ class MRZ { late String _optData; String? _optData2; - MRZ(Uint8List encodedMRZ) { + final Uint8List _encoded; + + MRZ(Uint8List encodedMRZ) : _encoded = encodedMRZ { _parse(encodedMRZ); } - static int calculateCheckDigit(String checkString) { + Uint8List toBytes() { + return _encoded; + } + + String toEncodedString() { + var data = toBytes(); + final inputStream = InputStream(data); + var result = _read(inputStream, data.length); + + return result; + } + + static int calculateCheckDigit(String checkString) { const charMap = { "0" : "0", "1" : "1", "2" : "2", "3" : "3", @@ -64,7 +79,7 @@ class MRZ { for (int i = 0; i < checkString.length; i++) { final lookup = charMap[checkString[i]]; final number = int.tryParse(lookup ?? ""); - if(number == null) { + if (number == null) { return 0; } @@ -80,41 +95,36 @@ class MRZ { if (data.length == 90) { version = MRZVersion.td1; _parseTD1(istream); - } - else if (data.length == 72) { + } else if (data.length == 72) { version = MRZVersion.td2; _parseTD2(istream); - } - else if (data.length == 88) { + } else if (data.length == 88) { version = MRZVersion.td3; _parseTD3(istream); - } - else { + } else { throw MRZParseError("Invalid MRZ data"); } } void _parseTD1(InputStream istream) { - documentCode = _read(istream, 2); - country = _read(istream, 3); - _docNum = _read(istream, 9); + documentCode = _read(istream, 2); + country = _read(istream, 3); + _docNum = _read(istream, 9); final cdDocNum = _readWithPad(istream, 1); - _optData = _read(istream, 15); - dateOfBirth = _readDate(istream); + _optData = _read(istream, 15); + dateOfBirth = _readDate(istream); _assertCheckDigit(dateOfBirth.formatYYMMDD(), _readCD(istream), - "Data of Birth check digit mismatch" - ); + "Data of Birth check digit mismatch"); - gender = _read(istream, 1); - dateOfExpiry = _readDate(istream); + gender = _read(istream, 1); + dateOfExpiry = _readDate(istream); _assertCheckDigit(dateOfExpiry.formatYYMMDD(), _readCD(istream), - "Data of Expiry check digit mismatch" - ); + "Data of Expiry check digit mismatch"); nationality = _read(istream, 3); - _optData2 = _read(istream, 11); + _optData2 = _read(istream, 11); _parseExtendedDocumentNumber(cdDocNum); final cdComposite = _readCD(istream); @@ -129,30 +139,26 @@ class MRZ { composite += _readWithPad(istream, 7); istream.skip(3); composite += _readWithPad(istream, 11); - _assertCheckDigit(composite, cdComposite, - "Composite check digit mismatch" - ); + _assertCheckDigit(composite, cdComposite, "Composite check digit mismatch"); } void _parseTD2(InputStream istream) { - documentCode = _read(istream, 2); - country = _read(istream, 3); + documentCode = _read(istream, 2); + country = _read(istream, 3); _setNames(_readNameIdentifiers(istream, 31)); - _docNum = _read(istream, 9); + _docNum = _read(istream, 9); final cdDocNum = _readWithPad(istream, 1); - nationality = _read(istream, 3); - dateOfBirth = _readDate(istream); + nationality = _read(istream, 3); + dateOfBirth = _readDate(istream); _assertCheckDigit(dateOfBirth.formatYYMMDD(), _readCD(istream), - "Data of Birth check digit mismatch" - ); + "Data of Birth check digit mismatch"); - gender = _read(istream, 1); - dateOfExpiry = _readDate(istream); + gender = _read(istream, 1); + dateOfExpiry = _readDate(istream); _assertCheckDigit(dateOfExpiry.formatYYMMDD(), _readCD(istream), - "Data of Expiry check digit mismatch" - ); + "Data of Expiry check digit mismatch"); _optData = _read(istream, 7); _parseExtendedDocumentNumber(cdDocNum); @@ -166,37 +172,31 @@ class MRZ { composite += _readWithPad(istream, 7); istream.skip(1); composite += _readWithPad(istream, 14); - _assertCheckDigit(composite, cdComposite, - "Composite check digit mismatch" - ); + _assertCheckDigit(composite, cdComposite, "Composite check digit mismatch"); } void _parseTD3(InputStream istream) { - documentCode = _read(istream, 2); - country = _read(istream, 3); + documentCode = _read(istream, 2); + country = _read(istream, 3); _setNames(_readNameIdentifiers(istream, 39)); _docNum = _read(istream, 9); - _assertCheckDigit(_docNum, _readCD(istream), - "Document Number check digit mismatch" - ); + _assertCheckDigit( + _docNum, _readCD(istream), "Document Number check digit mismatch"); - nationality = _read(istream, 3); - dateOfBirth = _readDate(istream); + nationality = _read(istream, 3); + dateOfBirth = _readDate(istream); _assertCheckDigit(dateOfBirth.formatYYMMDD(), _readCD(istream), - "Data of Birth check digit mismatch" - ); + "Data of Birth check digit mismatch"); - gender = _read(istream, 1); - dateOfExpiry = _readDate(istream); + gender = _read(istream, 1); + dateOfExpiry = _readDate(istream); _assertCheckDigit(dateOfExpiry.formatYYMMDD(), _readCD(istream), - "Data of Expiry check digit mismatch" - ); + "Data of Expiry check digit mismatch"); _optData = _read(istream, 14); - _assertCheckDigit(_optData, _readCD(istream), - "Optional data check digit mismatch" - ); + _assertCheckDigit( + _optData, _readCD(istream), "Optional data check digit mismatch"); final cdComposite = _readCD(istream); @@ -207,9 +207,7 @@ class MRZ { composite += _readWithPad(istream, 7); istream.skip(1); composite += _readWithPad(istream, 22); - _assertCheckDigit(composite, cdComposite, - "Composite check digit mismatch" - ); + _assertCheckDigit(composite, cdComposite, "Composite check digit mismatch"); } void _setNames(List nameIds) { @@ -223,21 +221,19 @@ class MRZ { void _parseExtendedDocumentNumber(String strCdDocNum) { int cdDocNum = 0; - if(strCdDocNum == '<' && _optData.length > 2) { + if (strCdDocNum == '<' && _optData.length > 2) { final dnSecondPart = _optData.split('<')[0]; _docNum += dnSecondPart.substring(0, dnSecondPart.length - 1); - cdDocNum = int.parse(dnSecondPart[dnSecondPart.length - 1]); - _optData = _optData2 ?? ''; + cdDocNum = int.parse(dnSecondPart[dnSecondPart.length - 1]); + _optData = _optData2 ?? ''; _optData2 = null; - } - else { + } else { cdDocNum = int.parse(strCdDocNum); } - _assertCheckDigit(_docNum, cdDocNum, - "Document Number check digit mismatch" - ); + _assertCheckDigit( + _docNum, cdDocNum, "Document Number check digit mismatch"); } static String _read(InputStream istream, int maxLength) { @@ -251,7 +247,8 @@ class MRZ { static int _readCD(InputStream istream) { var scd = _readWithPad(istream, 1); if (scd == '<') return 0; - return int.tryParse(scd) ?? (throw MRZParseError("Invalid check digit character in MRZ")); + return int.tryParse(scd) ?? + (throw MRZParseError("Invalid check digit character in MRZ")); } static List _readNameIdentifiers(InputStream istream, int maxLength) { @@ -272,4 +269,4 @@ class MRZ { throw MRZParseError(errorMsg); } } -} \ No newline at end of file +} From e0d07371b331f409f686018b2e4e2ac56cdb12ce Mon Sep 17 00:00:00 2001 From: Nejc Skerjanc Date: Wed, 28 Jun 2023 11:15:19 +0200 Subject: [PATCH 2/4] example is updated to flutter v3 --- example/lib/main.dart | 2 +- example/pubspec.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 7064d9a..f02976c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -560,7 +560,7 @@ class _MrtdHomePageState extends State { SizedBox(height: 40), _buildForm(context), SizedBox(height: 20), - PlatformButton( // btn Read MRTD + PlatformElevatedButton( // btn Read MRTD onPressed: _disabledInput() || !_mrzData.currentState!.validate() ? null : _readMRTD, child: PlatformText(_isReading ? 'Reading ...' : 'Read Passport'), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e07d5ee..355650f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,7 +15,7 @@ description: A new Flutter project. version: 1.2.1 environment: - sdk: '>=2.17.1 <3.0.0' + sdk: '>=2.17.6 <3.0.0' publish_to: none @@ -26,8 +26,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_platform_widgets: ^1.7.1 - intl: ^0.17.0 + flutter_platform_widgets: ^3.3.5 + intl: ^0.18.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From b254d1ce51aa119b6b4b8ebdd820724174925419 Mon Sep 17 00:00:00 2001 From: nejc-skerjanc Date: Wed, 28 Jun 2023 11:34:16 +0200 Subject: [PATCH 3/4] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5e2b56..2c8abc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,4 +31,4 @@ jobs: - run: flutter pub get - name: Flutter test - run: flutter test --no-sound-null-safety + run: flutter test From 175febcf714539800ff46ae1b4ccfee931102359 Mon Sep 17 00:00:00 2001 From: Nejc Skerjanc Date: Mon, 2 Oct 2023 11:12:40 +0200 Subject: [PATCH 4/4] uprgraded some libraries and kotlin version --- example/android/app/build.gradle | 4 +- example/android/build.gradle | 5 +- lib/src/constants/asn1_ber.dart | 0 lib/src/lds/asn1ObjectIdentifiers.dart | 103 +++++++++++++++++++++++++ lib/src/lds/substruct/pace_info.dart | 0 lib/src/types/exception.dart | 0 pubspec.yaml | 3 +- 7 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 lib/src/constants/asn1_ber.dart create mode 100644 lib/src/lds/asn1ObjectIdentifiers.dart create mode 100644 lib/src/lds/substruct/pace_info.dart create mode 100644 lib/src/types/exception.dart diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 399e99c..19aba9a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,7 +40,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.mrtdeg" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/build.gradle b/example/android/build.gradle index 6df3db1..3373f1d 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,6 @@ buildscript { - ext.kotlin_version = '1.6.10' + //ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.8.0' repositories { google() jcenter() @@ -26,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/src/constants/asn1_ber.dart b/lib/src/constants/asn1_ber.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/lds/asn1ObjectIdentifiers.dart b/lib/src/lds/asn1ObjectIdentifiers.dart new file mode 100644 index 0000000..da5750c --- /dev/null +++ b/lib/src/lds/asn1ObjectIdentifiers.dart @@ -0,0 +1,103 @@ +// Created by Nejc Skerjanc, copyright © 2023 ZeroPass. All rights reserved. + +import 'dart:typed_data'; +import 'package:dmrtd/extensions.dart'; +import 'package:logging/logging.dart'; +import 'package:pointycastle/asn1.dart'; +import 'package:pointycastle/asn1/object_identifiers_database.dart'; + +import '../../types/exception.dart'; + + +// here you can add additional object identifiers that are not defined in pointycastle library +List> customOIDS = [ +{ +'identifierString': '1.2.3.4.5', 'readableName': 'someName', 'identifier': [0, 1, 2, 3, 4, 5] +} +]; + + +// add additional object identifiers + +class ASN1ObjectIdentifierException implements DMRTDException { + final String message; + @override + String exceptionName = 'ASN1ObjectIdentifierException'; + + ASN1ObjectIdentifierException(this.message); + //@override + //String toString() { + // String result = 'ASN1ObjectIdentifierException'; + // if (message is String) return '$result: $message'; + // return result; + //} + + +} + + +class ASN1ObjectIdentifiers { + // object identifiers that are defined in pointycastle library + List> _OIDS = oi; + final _log = Logger("ASN1ObjectIdentifiers"); + + + ASN1ObjectIdentifiers(){ + // add custom object identifiers to existing ones + for (var customOID in customOIDS) { + if (!checkOID(item:customOID)){ + throw ASN1ObjectIdentifierException('Object identifier is not valid.'); + } + _OIDS.add(customOID); + } + } + + // check if object identifier is valid + bool checkOID({required Map item}){ + //check if list contains all required keys + if (!item.containsKey('identifier') || !item.containsKey('identifierString') || !item.containsKey('readableName')) { + _log.error('Object identifier must contain identifier, identifierString and readableName.'); + return false; + } + + if (item['identifier'] is! List) { + _log.error('Object identifier identifier must be List.'); + return false; + } + if (item['identifierString'] is! String) { + _log.error('Object identifier identifierString must be String.'); + return false; + } + if (item['readableName'] is! String) { + _log.error('Object identifier readableName must be String.'); + return false; + } + return true; + } + + + // has object identifier with identifier string + bool hasOIDWithIdentifierString(String identifierString) { + return _OIDS.any((element) => element['identifierString'] == identifierString); + } + + + // get object identifier by identifier string + Map getOIDByIdentifierString(String identifierString) { + return _OIDS.firstWhere((element) => element['identifierString'] == identifierString, orElse: () => + throw ASN1ObjectIdentifierException('Object identifier with identifier string $identifierString does not exist.')); + } + + // has object identifier wih identifier + bool hasOIDWithIdentifier(List identifier) { + return _OIDS.any((element) => element['identifier'] == identifier); + } + + // get object identifier by identifier + Map getOIDByIdentifier(List identifier) { + return _OIDS.firstWhere((element) => element['identifier'] == identifier, orElse: () => + throw ASN1ObjectIdentifierException('Object identifier with identifier $identifier does not exist.')); + } + +} + diff --git a/lib/src/lds/substruct/pace_info.dart b/lib/src/lds/substruct/pace_info.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/types/exception.dart b/lib/src/types/exception.dart new file mode 100644 index 0000000..e69de29 diff --git a/pubspec.yaml b/pubspec.yaml index 1de56cd..f44b08e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,10 +14,11 @@ dependencies: crypto: ^3.0.1 expandable: ^5.0.1 fixnum: ^1.0.0 - flutter_nfc_kit: '^3.1.0' + flutter_nfc_kit: ^3.3.1 logging: ^1.0.1 meta: ^1.3.0 tripledes_nullsafety: ^1.0.3 + pointycastle: ^3.7.3 dev_dependencies: flutter: # needed for flutter_nfc_kit