From ab73ee62600fbe15275954b1891be18c2ed25fae Mon Sep 17 00:00:00 2001 From: Lizard Date: Thu, 29 Aug 2024 20:50:09 +0600 Subject: [PATCH] feat: v0.1.2 | Add support for DG11 and DG12 --- README.md | 11 ++-- jsr.json | 2 +- package.json | 2 +- src/consts/interfaces.ts | 52 +++++++++++++++++ src/dg11.ts | 119 +++++++++++++++++++++++++++++++++++++++ src/dg12.ts | 93 ++++++++++++++++++++++++++++++ src/dg2.ts | 10 ++++ src/dg3.ts | 10 ++++ src/index.ts | 2 + tests/dgs/EF_DG11.bin | Bin 0 -> 139 bytes tests/dgs/EF_DG12.bin | Bin 0 -> 69 bytes tests/index.test.ts | 32 ++++++++++- 12 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 src/dg11.ts create mode 100644 src/dg12.ts create mode 100644 tests/dgs/EF_DG11.bin create mode 100644 tests/dgs/EF_DG12.bin diff --git a/README.md b/README.md index b0e78cb..5a0dd37 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@

- +
+ +

@@ -26,7 +28,7 @@ npx jsr add @li0ard/tsemrtd # for npm ## Usage ```ts -import { DG1 } from "tsemrtd" +import { DG1 } from "@li0ard/tsemrtd" let file = await Bun.file("EF_DG1.bin").bytes() let data = DG1.load(Buffer.from(file)) @@ -43,6 +45,5 @@ console.log(data) - [x] DG3 - [x] DG5 - [x] DG7 -- [ ] DG11 -- [ ] DG12 -- [ ] DG16 +- [x] DG11 +- [x] DG12 \ No newline at end of file diff --git a/jsr.json b/jsr.json index 3eb459f..f68b8dc 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@li0ard/tsemrtd", - "version": "0.1.1", + "version": "0.1.2", "exports": "./src/index.ts", "publish": { "include": [ diff --git a/package.json b/package.json index 5099324..3f7fb39 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tsemrtd", "module": "index.ts", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "author": "li0ard", "repository": { diff --git a/src/consts/interfaces.ts b/src/consts/interfaces.ts index 5923d36..de7d42b 100644 --- a/src/consts/interfaces.ts +++ b/src/consts/interfaces.ts @@ -87,4 +87,56 @@ export interface DecodedFingerprint extends AbstractBioTemplate { nrOfRepresention: number, /** Name of finger/part of palm */ fingerType: FingerType +} + +/** Decoded EF.DG11 datagroup */ +export interface DecodedAdditionalPersonalData { + /** Full name of document holder */ + nameOfHolder: string, + /** Other names of document holder */ + otherNames: string[], + /** Personal number */ + personalNumber: string, + /** Full date of birth (CCYYMMDD) */ + fullDateOfBirth: number, + /** Place of birth */ + placeOfBirth: string[], + /** Permanent residence address */ + permanentAddress: string[], + /** Phone number */ + telephone: string, + /** Profession */ + profession: string, + /** Position */ + title: string, + /** Personal resume */ + personalSummary: string, + /** Proof of citizenship. Image described by ISO/IEC 10918 */ + proofOfCitizenship: Buffer, + /** Numbers of other valid TDs */ + otherValidTDNumbers: string[], + /** Information about detention */ + custodyInformation: string +} + +/** Decoded EF.DG12 datagroup */ +export interface DecodedAdditionalDocumentData { + /** Full date of issue (YYYYMMDD) */ + dateOfIssue: number, + /** Issuing authority */ + issuingAuthority: string, + /** Name of another person, formatted according to the rules of Doc 9303 */ + namesOfOtherPersons: string[], + /** Endorsements and observations */ + endorsements: string, + /** Tax and exit requirements */ + taxAndExitReqs: string, + /** Image of front of document. Image described by ISO/IEC 10918 */ + imageOfFront: Buffer, + /** Image of rear of document. Image described by ISO/IEC 10918 */ + imageOfRear: Buffer, + /** Date and time of document personalization (YYYYMMDDHHMMSS) */ + dateOfPersonalization: number, + /** Serial number of personalization system */ + personalizationNumber: string } \ No newline at end of file diff --git a/src/dg11.ts b/src/dg11.ts new file mode 100644 index 0000000..c2107ea --- /dev/null +++ b/src/dg11.ts @@ -0,0 +1,119 @@ +import TLV from "node-tlv" +import { Enums, Interfaces } from "./index"; + +/** + * Class for working with DG11 (Additional personal data) +*/ +export class DG11 { + /** + * Get additional personal data + * @param data Data of EF.DG11 file + */ + static load(data: string | Buffer): Interfaces.DecodedAdditionalPersonalData { + const FULL_NAME_TAG = 0x5F0E; + const OTHER_NAME_TAG = 0x5F0F; + const OTHER_NAME_ARRAY_TAG = 0xA0; + const PERSONAL_NUMBER_TAG = 0x5F10; + + // In 'CCYYMMDD' format. + const FULL_DATE_OF_BIRTH_TAG = 0x5F2B; + + // Fields separated by '<' + const PLACE_OF_BIRTH_TAG = 0x5F11; + + // Fields separated by '<' + const PERMANENT_ADDRESS_TAG = 0x5F42; + const TELEPHONE_TAG = 0x5F12; + const PROFESSION_TAG = 0x5F13; + const TITLE_TAG = 0x5F14; + const PERSONAL_SUMMARY_TAG = 0x5F15; + + // Compressed image per ISO/IEC 10918 + const PROOF_OF_CITIZENSHIP_TAG = 0x5F16; + + // Separated by '<' + const OTHER_VALID_TD_NUMBERS_TAG = 0x5F17; + const CUSTODY_INFORMATION_TAG = 0x5F18; + + let nameOfHolder: string = "", + otherNames: string[] = [], + personalNumber: string = "", + fullDateOfBirth: number = 0, + placeOfBirth: string[] = [], + permanentAddress: string[] = [], + telephone: string = "", + profession: string = "", + title: string = "", + personalSummary: string = "", + proofOfCitizenship: Buffer = Buffer.from(""), + otherValidTDNumbers: string[] = [], + custodyInformation: string = ""; + + + let tlv = TLV.parse(data) + if(parseInt(tlv.tag, 16) != Enums.TAGS.DG11) throw new Error(`Invalid DG1 tag "0x${tlv.tag}", expected 0x${Enums.TAGS.DG11.toString(16)}`); + + for(let i of tlv.child) { + switch(parseInt(i.tag, 16)) { + case FULL_NAME_TAG: + nameOfHolder = i.bValue.toString("utf-8") + break; + case PERSONAL_NUMBER_TAG: + personalNumber = i.bValue.toString("utf-8") + break; + case OTHER_NAME_ARRAY_TAG: + for(let j of i.child) { + if(parseInt(j.tag, 16) == OTHER_NAME_TAG) { + otherNames.push(j.bValue.toString("utf-8")) + } + } + break; + case FULL_DATE_OF_BIRTH_TAG: + fullDateOfBirth = parseInt(i.bValue.toString("hex")) + break; + case PLACE_OF_BIRTH_TAG: + placeOfBirth = i.bValue.toString("utf-8").split("<") + break; + case PERMANENT_ADDRESS_TAG: + permanentAddress = i.bValue.toString("utf-8").split("<") + break; + case TELEPHONE_TAG: + telephone = i.bValue.toString("utf-8") + break + case PROFESSION_TAG: + profession = i.bValue.toString("utf-8") + break; + case TITLE_TAG: + title = i.bValue.toString("utf-8") + break; + case PERSONAL_SUMMARY_TAG: + personalSummary = i.bValue.toString("utf-8") + break; + case PROOF_OF_CITIZENSHIP_TAG: + proofOfCitizenship = i.bValue + break; + case OTHER_VALID_TD_NUMBERS_TAG: + otherValidTDNumbers = i.bValue.toString("utf-8").split("<") + break; + case CUSTODY_INFORMATION_TAG: + custodyInformation = i.bValue.toString("utf-8") + break; + } + } + return { + nameOfHolder, + otherNames, + personalNumber, + fullDateOfBirth, + placeOfBirth, + permanentAddress, + telephone, + profession, + title, + personalSummary, + proofOfCitizenship, + otherValidTDNumbers, + custodyInformation + } + } +} \ No newline at end of file diff --git a/src/dg12.ts b/src/dg12.ts new file mode 100644 index 0000000..0278cd7 --- /dev/null +++ b/src/dg12.ts @@ -0,0 +1,93 @@ +import TLV from "node-tlv" +import { Enums, Interfaces } from "./index"; + +/** + * Class for working with DG12 (Additional document data) +*/ +export class DG12 { + /** + * Get additional document data + * @param data Data of EF.DG12 file + */ + static load(data: string | Buffer): Interfaces.DecodedAdditionalDocumentData { + const ISSUING_AUTHORITY_TAG = 0x5F19; + + // yyyymmdd + const DATE_OF_ISSUE_TAG = 0x5F26; + + // formatted per ICAO 9303 rules + const NAME_OF_OTHER_PERSON_ARRAY_TAG = 0xA0; + const NAME_OF_OTHER_PERSON_TAG = 0x5F1A; + const ENDORSEMENTS_AND_OBSERVATIONS_TAG = 0x5F1B; + const TAX_OR_EXIT_REQUIREMENTS_TAG = 0x5F1C; + + // Image per ISO/IEC 10918 + const IMAGE_OF_FRONT_TAG = 0x5F1D; + const IMAGE_OF_REAR_TAG = 0x5F1E; + + // yyyymmddhhmmss + const DATE_AND_TIME_OF_PERSONALIZATION = 0x5F55; + const PERSONALIZATION_SYSTEM_SERIAL_NUMBER_TAG = 0x5F56; + + let dateOfIssue: number = 0, + issuingAuthority: string = "", + namesOfOtherPersons: string[] = [], + endorsements: string = "", + taxAndExitReqs: string = "", + imageOfFront: Buffer = Buffer.from(""), + imageOfRear: Buffer = Buffer.from(""), + dateOfPersonalization: number = 0, + personalizationNumber: string = "" + + let tlv = TLV.parse(data) + if(parseInt(tlv.tag, 16) != Enums.TAGS.DG12) throw new Error(`Invalid DG1 tag "0x${tlv.tag}", expected 0x${Enums.TAGS.DG12.toString(16)}`); + + for(let i of tlv.child) { + switch(parseInt(i.tag, 16)) { + case ISSUING_AUTHORITY_TAG: + issuingAuthority = i.bValue.toString("utf-8") + break; + case DATE_OF_ISSUE_TAG: + dateOfIssue = parseInt(i.bValue.toString("hex")) + break; + case NAME_OF_OTHER_PERSON_ARRAY_TAG: + for(let j of i.child) { + if(parseInt(j.tag, 16) == NAME_OF_OTHER_PERSON_TAG) { + namesOfOtherPersons.push(j.bValue.toString("utf-8")) + } + } + break; + case ENDORSEMENTS_AND_OBSERVATIONS_TAG: + endorsements = i.bValue.toString("utf-8") + break; + case TAX_OR_EXIT_REQUIREMENTS_TAG: + taxAndExitReqs = i.bValue.toString("utf-8") + break; + case IMAGE_OF_FRONT_TAG: + imageOfFront = i.bValue; + break; + case IMAGE_OF_REAR_TAG: + imageOfRear = i.bValue; + break; + case DATE_AND_TIME_OF_PERSONALIZATION: + dateOfPersonalization = parseInt(i.bValue.toString("hex")) + break; + case PERSONALIZATION_SYSTEM_SERIAL_NUMBER_TAG: + personalizationNumber = i.bValue.toString("utf-8") + break; + } + } + + return { + dateOfIssue, + issuingAuthority, + namesOfOtherPersons, + endorsements, + taxAndExitReqs, + imageOfFront, + imageOfRear, + dateOfPersonalization, + personalizationNumber + } + } +} \ No newline at end of file diff --git a/src/dg2.ts b/src/dg2.ts index 79ed5c4..9c726b6 100644 --- a/src/dg2.ts +++ b/src/dg2.ts @@ -5,6 +5,12 @@ import { Enums, Interfaces } from "./index"; * Class for working with DG2 (Face) */ export class DG2 { + /** + * Extract int from buffer + * @param data Buffer + * @param start Offset + * @param end Offset+length + */ private extractContent(data: Buffer, start: number, end: number): number { if (end - start == 1) { return data.subarray(start, end).readInt8(); @@ -13,6 +19,10 @@ export class DG2 { } return data.subarray(start,end).readInt32BE() } + /** + * Read Biometric data block + * @param tlv + */ readBDB(tlv: TLV): Interfaces.DecodedImage { if(parseInt(tlv.tag, 16) != 0x7f60) throw new Error(`Invalid object tag "0x${tlv.tag}", expected 0x7f60`); diff --git a/src/dg3.ts b/src/dg3.ts index 81ea7a9..762d5e6 100644 --- a/src/dg3.ts +++ b/src/dg3.ts @@ -6,6 +6,12 @@ import { Enums, Interfaces } from "./index"; * @experimental */ export class DG3 { + /** + * Extract int from buffer + * @param data Buffer + * @param start Offset + * @param end Offset+length + */ private extractContent(data: Buffer, start: number, end: number): number { if (end - start == 1) { return data.subarray(start, end).readUInt8(); @@ -17,6 +23,10 @@ export class DG3 { } return parseInt(data.subarray(start,end).toString("hex"), 16) } + /** + * Read Biometric data block + * @param tlv + */ readBDB(tlv: TLV): Interfaces.DecodedFingerprint { if(parseInt(tlv.tag, 16) != 0x7f60) throw new Error(`Invalid object tag "0x${tlv.tag}", expected 0x7f60`); let sbh = tlv.child[0] diff --git a/src/index.ts b/src/index.ts index e6dca6d..011cf8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,7 @@ export { DG2 } from "./dg2" export { DG3 } from "./dg3" export { DG5 } from "./dg5" export { DG7 } from "./dg7" +export { DG11 } from "./dg11" +export { DG12 } from "./dg12" export * as Enums from "./consts/enums" export * as Interfaces from "./consts/interfaces" \ No newline at end of file diff --git a/tests/dgs/EF_DG11.bin b/tests/dgs/EF_DG11.bin new file mode 100644 index 0000000000000000000000000000000000000000..73a6854e3767a079fae3693cb02b490027b6e266 GIT binary patch literal 139 zcmd09?1&MM=ZhDJ*NzvAcZwH^7mgQ+7mXK-msk+b$KjHns^FEMk+*=CiIFj$pFKP? zCnqy8H(r3*(8xGmn?K0sy-bA?E-9 literal 0 HcmV?d00001 diff --git a/tests/dgs/EF_DG12.bin b/tests/dgs/EF_DG12.bin new file mode 100644 index 0000000000000000000000000000000000000000..011e90a772cc405a788d4c2094ea52f159c2fefd GIT binary patch literal 69 zcmd0Cj+bN!aSaZMS7T97VPssu%f!eSFU207nUj;5m>Vw*k(7Z@vWyJzazGr)4pJ** M5E&oFY-nT*02#0jod5s; literal 0 HcmV?d00001 diff --git a/tests/index.test.ts b/tests/index.test.ts index dd96026..d27528a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test" import { join } from "path" -import { COM, DG1, DG2, DG3, DG5, DG7 } from "../src" +import { COM, DG1, DG2, DG3, DG5, DG7, DG11, DG12 } from "../src" const getDGContent = async (name: string): Promise => { return Buffer.from(await Bun.file(join(import.meta.dir, "dgs", name)).bytes()) @@ -47,4 +47,34 @@ test("DG7", async () => { expect( new Bun.CryptoHasher("sha256").update(data[0]).digest().toString("hex") ).toBe("a3c5801a3692cf43495e391dc70d32e41526b4a5f700d5566c64688e897482e5") +}) + +test("DG11", async () => { + let data = DG11.load(await getDGContent("EF_DG11.bin")) + expect(data.nameOfHolder).toBe("Doe John") + expect(data.otherNames[0]).toBe("William") + expect(data.personalNumber).toBe("123") + expect(data.fullDateOfBirth).toBe(19700101) + expect(data.placeOfBirth[0]).toBe("TEST") + expect(data.placeOfBirth[1]).toBe("TEST") + expect(data.permanentAddress[0]).toBe("TEST") + expect(data.permanentAddress[1]).toBe("TEST") + expect(data.telephone).toBe("123") + expect(data.profession).toBe("TEST") + expect(data.title).toBe("TEST") + expect(data.personalSummary).toBe("TEST") + expect(data.otherValidTDNumbers[0]).toBe("123") + expect(data.otherValidTDNumbers[1]).toBe("123") + expect(data.custodyInformation).toBe("TEST") +}) + +test("DG12", async () => { + let data = DG12.load(await getDGContent("EF_DG12.bin")) + expect(data.dateOfIssue).toBe(20240101) + expect(data.issuingAuthority).toBe("TEST") + expect(data.namesOfOtherPersons[0]).toBe("William") + expect(data.endorsements).toBe("TEST") + expect(data.taxAndExitReqs).toBe("TEST") + expect(data.dateOfPersonalization).toBe(20240101123059) + expect(data.personalizationNumber).toBe("123") }) \ No newline at end of file