Skip to content

Instantly share code, notes, and snippets.

@tkaczenko
Last active January 18, 2020 22:25
Show Gist options
  • Save tkaczenko/983615b2e5c6feaa1940069c1e28bf55 to your computer and use it in GitHub Desktop.
Save tkaczenko/983615b2e5c6feaa1940069c1e28bf55 to your computer and use it in GitHub Desktop.
Чтение данных биометрического паспорта (на примере id-карты Украины) с использованием NFC ридера и JMRTD / Reading data from biometric passport (for example, Ukrainian ePassport or Ukrainian ID) using NFC reader with JMRTD
package com.github.tkaczenko.cardreader.service;
import lombok.RequiredArgsConstructor;
import net.sf.scuba.smartcards.CardServiceException;
import org.jmrtd.PACEKeySpec;
import org.jmrtd.PassportService;
import org.jmrtd.lds.CardAccessFile;
import org.jmrtd.lds.PACEInfo;
import org.jmrtd.lds.SecurityInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Andrii Tkachenko
*/
@RequiredArgsConstructor
@Validated
@Service
public class DataConnection {
private static final Logger LOGGER = LoggerFactory.getLogger(DataConnection.class);
private final PassportService ps;
public boolean initConnection(@NotNull String can) throws CardServiceException, IOException {
ps.open();
CardAccessFile cardAccessFile = new CardAccessFile(ps.getInputStream(PassportService.EF_CARD_ACCESS));
Collection<SecurityInfo> securityInfos = cardAccessFile.getSecurityInfos();
SecurityInfo securityInfo = securityInfos.iterator().next();
LOGGER.info("ProtocolOIDString: " + securityInfo.getProtocolOIDString());
LOGGER.info("ObjectIdentifier: " + securityInfo.getObjectIdentifier());
List<PACEInfo> paceInfos = getPACEInfos(securityInfos);
LOGGER.debug("Found a card access file: paceInfos (" + (paceInfos == null ? 0 : paceInfos.size()) + ") = " + paceInfos);
if (paceInfos != null && paceInfos.size() > 0) {
PACEInfo paceInfo = paceInfos.get(0);
PACEKeySpec paceKey = PACEKeySpec.createCANKey(can);
ps.doPACE(paceKey, paceInfo.getObjectIdentifier(), PACEInfo.toParameterSpec(paceInfo.getParameterId()), paceInfo.getParameterId());
ps.sendSelectApplet(true);
return true;
} else {
ps.close();
return false;
}
}
private List<PACEInfo> getPACEInfos(Collection<SecurityInfo> securityInfos) {
return securityInfos.stream()
.filter(securityInfo -> securityInfo instanceof PACEInfo)
.map(securityInfo -> (PACEInfo) securityInfo)
.collect(Collectors.toList());
}
}
package com.github.tkaczenko.cardreader.service;
import com.github.tkaczenko.cardreader.model.Person;
import lombok.RequiredArgsConstructor;
import net.sf.scuba.smartcards.CardServiceException;
import org.jmrtd.PassportService;
import org.jmrtd.lds.LDSFileUtil;
import org.jmrtd.lds.icao.*;
import org.jmrtd.lds.iso19794.FaceImageInfo;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author Andrii Tkachenko
*/
@RequiredArgsConstructor
@Service
public class DataReader {
private static SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd");
private final PassportService ps;
public Person read() throws CardServiceException, IOException, ParseException {
DG1File dg1 = (DG1File) LDSFileUtil.getLDSFile(PassportService.EF_DG1, ps.getInputStream(PassportService.EF_DG1));
DG11File dg11 = (DG11File) LDSFileUtil.getLDSFile(PassportService.EF_DG11, ps.getInputStream(PassportService.EF_DG11));
DG12File dg12 = (DG12File) LDSFileUtil.getLDSFile(PassportService.EF_DG12, ps.getInputStream(PassportService.EF_DG12));
DG2File dg2 = (DG2File) LDSFileUtil.getLDSFile(PassportService.EF_DG2, ps.getInputStream(PassportService.EF_DG2));
FaceImageInfo faceInfo = dg2.getFaceInfos().stream()
.flatMap(fi -> fi.getFaceImageInfos().stream())
.findFirst()
.orElse(null);
MRZInfo mrzInfo = dg1.getMRZInfo();
return Person.builder()
.names(dg11.getNameOfHolder())
.fathersName(dg11.getOtherNames())
.dateOfBirth(date.parse(dg11.getFullDateOfBirth()))
.placeOfBirth(dg11.getPlaceOfBirth())
.gender(mrzInfo.getGender().toString())
.nationality(mrzInfo.getNationality())
.docNumber(mrzInfo.getDocumentNumber())
.docDateOExpiry(mrzInfo.getDateOfExpiry())
.docIssuingAuthority(dg12.getIssuingAuthority())
.docDateOfIssue(date.parse(dg12.getDateOfIssue()))
.faceInfo(faceInfo)
.build();
}
}
package com.github.tkaczenko.cardreader.service;
import org.jmrtd.lds.iso19794.FaceImageInfo;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.imageio.ImageIO;
import javax.validation.constraints.NotNull;
import java.awt.image.BufferedImage;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author Andrii Tkachenko
*/
@Validated
@Service
public class ImageSaver {
public String saveFaceImage(@NotNull FaceImageInfo imageInfo, String destination) throws IOException {
String filePath = "%s/%s";
int imageLength = imageInfo.getImageLength();
DataInputStream dataInputStream = new DataInputStream(imageInfo.getImageInputStream());
byte[] buffer = new byte[imageLength];
dataInputStream.readFully(buffer, 0, imageLength);
FileOutputStream fileOut2 = new FileOutputStream(String.format(filePath, destination, "tmp.jp2"));
fileOut2.write(buffer);
fileOut2.flush();
fileOut2.close();
dataInputStream.close();
File tempFile = new File(String.format(filePath, destination, "tmp.jp2"));
BufferedImage nImage = ImageIO.read(tempFile);
if (tempFile.exists()) {
tempFile.delete();
}
File output = new File(String.format(filePath, destination, "facePhoto.jpg"));
ImageIO.write(nImage, "jpg", output);
return output.getAbsolutePath();
}
}
package com.github.tkaczenko.cardreader.model;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import org.jmrtd.lds.iso19794.FaceImageInfo;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Andrii Tkachenko
*/
@Getter
@ToString
@Builder
public class Person implements Serializable {
private String firstName;
private String lastName;
private String ukFirstName;
private String ukLastName;
private String fathersName;
private Date dateOfBirth;
private String placeOfBirth;
private String gender;
private String nationality;
private String docNumber;
private String docDateOExpiry;
private Date docDateOfIssue;
private String docIssuingAuthority;
private FaceImageInfo faceInfo;
public static class PersonBuilder {
public PersonBuilder names(String names) {
String[] namesArr = names.split("<");
ukFirstName = getNameByPos(namesArr, 1);
ukLastName = getNameByPos(namesArr, 0);
firstName = getNameByPos(namesArr, 4);
lastName = getNameByPos(namesArr, 3);
return this;
}
public PersonBuilder fathersName(List<String> otherNames) {
if (otherNames.size() > 0) {
fathersName = otherNames.get(0);
}
return this;
}
public PersonBuilder placeOfBirth(List<String> placeOfBirthList) {
placeOfBirth = placeOfBirthList.stream()
.map(String::toString)
.collect(Collectors.joining(" "));
return this;
}
private String getNameByPos(String[] namesArr, int pos) {
return namesArr.length > pos ? namesArr[pos].replaceAll("<", "") : "";
}
}
}
package com.github.tkaczenko.cardreader;
import com.github.tkaczenko.cardreader.model.Person;
import com.github.tkaczenko.cardreader.service.DataConnection;
import com.github.tkaczenko.cardreader.service.DataReader;
import com.github.tkaczenko.cardreader.service.ImageSaver;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.io.File;
/**
* @author Andrii Tkachenko
*/
@RequiredArgsConstructor
@SpringBootApplication
public class ReadUkrPassportApplication {
private static final Logger LOGGER = LoggerFactory.getLogger(ReadUkrPassportApplication.class);
private final DataConnection dataConnection;
private final DataReader dataReader;
private final ImageSaver imageSaver;
public static void main(String[] args) {
SpringApplication.run(ReadUkrPassportApplication.class, args);
}
@Bean
CommandLineRunner lookup() {
return args -> {
// Last 6 digits of Document No.
String can = args[0];
if (dataConnection.initConnection(can)) {
Person person = dataReader.read();
LOGGER.info("Read data: {}", person);
String path = imageSaver.saveFaceImage(person.getFaceInfo(), new File(".").getCanonicalPath());
LOGGER.info("Saved photo image to {}", path);
} else {
LOGGER.info("Cannot establish a connection to read data with PACE protocol by CAN key");
}
};
}
}
package com.github.tkaczenko.cardreader.cfg;
import net.sf.scuba.smartcards.CardService;
import org.jmrtd.PassportService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.TerminalFactory;
/**
* @author Andrii Tkachenko
*/
@Configuration
public class ReadUkrPassportConfig {
@Bean
public CardTerminal cardTerminal() throws CardException {
return TerminalFactory.getDefault().terminals().list().stream()
.filter(cardTerminal -> {
try {
return cardTerminal.isCardPresent();
} catch (CardException e) {
e.printStackTrace();
return false;
}
})
.findFirst()
.orElseThrow(RuntimeException::new);
}
@Bean
public CardService cardService(@Qualifier("cardTerminal") CardTerminal cardTerminal) {
return CardService.getInstance(cardTerminal);
}
@Bean
public PassportService passportService(@Qualifier("cardService") CardService cardService) {
return new PassportService(cardService, 256, 224, false, true);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment