Java tutorial
/* * See http://lino-framework.org/eidreader * * Copyright 2013-2014 Luc Saffre * * This file is part of the EIDReader project. * EIDReader is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * EIDReader is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with EIDReader; if not, see <http://www.gnu.org/licenses/>. * * The original version was largely inspired by a blog post by Revo at * Codeborne: * http://blog.codeborne.com/2010/10/javaxsmartcardio-and-esteid.html * * Parts of this code are adapted excerpts from * <https://code.google.com/p/eid-applet> * Copyright (C) 2008-2010 FedICT. * Copyright (C) 2009 Frank Cornelis. * */ package src.eidreader; import java.applet.Applet; import java.security.Permission; import java.io.FilePermission; import java.io.UnsupportedEncodingException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.List; import java.util.Calendar; //~ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.AbstractMap; import java.util.HashMap; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; import javax.smartcardio.CardPermission; import javax.smartcardio.CardChannel; import javax.smartcardio.CardException; import javax.smartcardio.CommandAPDU; import javax.smartcardio.ResponseAPDU; import javax.smartcardio.TerminalFactory; import javax.smartcardio.CardTerminal; import javax.smartcardio.Card; import javax.smartcardio.ATR; import java.security.AccessController; import java.security.PrivilegedAction; // 20131213 import be.fedict.eid.applet.service.impl.tlv.TlvParser; import be.fedict.eid.applet.service.Address; import be.fedict.eid.applet.service.Identity; import org.apache.commons.codec.binary.Base64; class EstEIDUtil { public static String bytesToString(byte[] data, final String enc) { try { return new String(data, enc); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Encoding " + enc + " not supported"); } } public static byte[] sendCommand(CardChannel channel, CommandAPDU command) throws CardException { ResponseAPDU responseAPDU = channel.transmit(command); int responseStatus = responseAPDU.getSW(); if (!isResponseOk(responseStatus)) { throw new RuntimeException("Error code: " + responseStatus); } return responseAPDU.getData(); } private static boolean isResponseOk(int responseStatus) { return responseStatus == 0x9000; } } class PersonalFile { static final String ENCODING = "windows-1252"; String[] data = new String[16]; public static final CommandAPDU SELECT_MASTER_FILE = new CommandAPDU( new byte[] { 0x00, (byte) 0xA4, 0x00, 0x0C }); public static final CommandAPDU SELECT_FILE_EEEE = new CommandAPDU( new byte[] { 0x00, (byte) 0xA4, 0x01, 0x0C, 0x02, (byte) 0xEE, (byte) 0xEE }); public static final CommandAPDU SELECT_FILE_5044 = new CommandAPDU( new byte[] { 0x00, (byte) 0xA4, 0x02, 0x04, 0x02, (byte) 0x50, (byte) 0x44 }); public static final String[] fields = new String[] { "last_name", "first_name", "other_names", "gender", "nationality", "birth_date", "national_id", "card_id", "valid_until", "birth_place", "date_issued", "ResidencePermitType", "remark1", "remark2", "remark3", "remark4" }; public PersonalFile(CardChannel channel) throws CardException { init(channel); } public String resultAsString(Boolean full) { String s = "reader: EE\n"; for (byte i = 0; i < 16; i++) { s = s + fields[i] + ": " + data[i].trim() + "\n"; } return s; } private void init(CardChannel channel) throws CardException { EstEIDUtil.sendCommand(channel, SELECT_MASTER_FILE); EstEIDUtil.sendCommand(channel, SELECT_FILE_EEEE); EstEIDUtil.sendCommand(channel, SELECT_FILE_5044); for (byte i = 1; i <= 16; i++) { data[i - 1] = extractField(channel, i); } } private String extractField(CardChannel channel, byte fieldNumber) throws CardException { return EstEIDUtil.bytesToString(EstEIDUtil.sendCommand(channel, new CommandAPDU(new byte[] { 0x00, (byte) 0xB2, fieldNumber, 0x04, 0x00 })), ENCODING); } } class BelgianReader { public static final String ENCODING = "utf-8"; //~ static final byte FIELD_COUNT = 18; private CardChannel cardChannel; private Identity identity; private Address address; private byte[] photo; //~ String[] data = new String[FIELD_COUNT]; //~ public static final String[] fields = new String[] { //~ "last_name","first_name", "other_names", "gender", "nationality", //~ "birth_date","national_id","card_id", //~ "valid_from", //~ "valid_until", //~ "street","zip_code", //~ "birth_place", "date_issued", "ResidencePermitType", //~ "remark1", //~ "remark2", //~ "remark3", //~ "remark4" //~ }; private static final int BLOCK_SIZE = 0xff; private final static byte[] ATR_PATTERN = new byte[] { 0x3b, (byte) 0x98, 0x00, 0x40, 0x00, (byte) 0x00, 0x00, 0x00, 0x01, 0x01, (byte) 0xad, 0x13, 0x10 }; private final static byte[] ATR_MASK = new byte[] { (byte) 0xff, (byte) 0xff, 0x00, (byte) 0xff, 0x00, 0x00, 0x00, 0x00, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xf0 }; public static final byte[] IDENTITY_FILE_ID = new byte[] { 0x3F, 0x00, (byte) 0xDF, 0x01, 0x40, 0x31 }; public static final byte[] ADDRESS_FILE_ID = new byte[] { 0x3F, 0x00, (byte) 0xDF, 0x01, 0x40, 0x33 }; public static final byte[] PHOTO_FILE_ID = new byte[] { 0x3F, 0x00, (byte) 0xDF, 0x01, 0x40, 0x35 }; public static boolean matchesEidAtr(ATR atr) { // from eid-applet-core...PcscEid.java byte[] atrBytes = atr.getBytes(); if (atrBytes.length != ATR_PATTERN.length) { return false; } for (int idx = 0; idx < atrBytes.length; idx++) { atrBytes[idx] &= ATR_MASK[idx]; } if (Arrays.equals(atrBytes, ATR_PATTERN)) { return true; } return false; } public static final String OOPS = "Unexpected end of TLV structure source"; //~ static final DateFormat date_format = DateFormat.getDateInstance(); static final SimpleDateFormat date_format = new SimpleDateFormat("yyyy-MM-dd"); public static String str2yaml(String s) { //~ if (s.length == 0): return "\"\""; return s; } public static String cal2date(Calendar cal) { return date_format.format(cal.getTime()); //~ return String.format("%s-%s-%s",cal.YEAR, cal.MONTH, cal.DAY_OF_MONTH); } final public boolean includePhoto = true; public BelgianReader(CardChannel channel) throws CardException, IOException, UnsupportedEncodingException { System.err.println("BelgianReader() constructor started"); this.cardChannel = channel; // 20131213 byte[] identityData = readFile(IDENTITY_FILE_ID); System.err.println("identityData has been read"); this.identity = TlvParser.parse(identityData, Identity.class); System.err.println("identityData has been parsed"); byte[] addressData = readFile(ADDRESS_FILE_ID); System.err.println("addressData has been read"); this.address = TlvParser.parse(addressData, Address.class); System.err.println("addressData has been parsed"); if (includePhoto) { this.photo = readFile(PHOTO_FILE_ID); } //~ HashMap identity = tlv2map(readFile(IDENTITY_FILE_ID)); //~ HashMap address = tlv2map(readFile(ADDRESS_FILE_ID)); //~ data[6] = identity[6]; //~ data[0] = identity.toString(); //~ data[1] = address.toString(); //~ data[7] = identity.cardNumber; // card_id //~ data[8] = cal2date(identity.cardValidityDateBegin); //valid_from //~ data[9] = cal2date(identity.cardValidityDateBegin); // valid_until //~ data[10] = address.streetAndNumber; // street //~ data[11] = address.zip; // zip_code //~ ByteArrayOutputStream baos = new ByteArrayOutputStream(); //~ byte[] identityFile = readFile(IDENTITY_FILE_ID); //~ System.err.println("identityFile.length is " + Integer.toString(identityFile.length)); //~ baos.write(identityFile); //~ //~ byte[] addressFile = readFile(ADDRESS_FILE_ID); //~ baos.write(addressFile); //~ System.err.println("addressFile.length is " + Integer.toString(addressFile.length)); //~ data[0] = baos.toString("utf-8"); //~ data[0] = baos.toString("ISO-8859-1 "); //~ data[0] = baos.toString("windows-1252"); //~ data[0] = baos.toString(); //~ EstEIDUtil.bytesToString } public byte[] readFile(byte[] fileId) throws CardException, IOException { selectFile(fileId); return readBinary(); } private void selectFile(byte[] fileId) throws CardException, FileNotFoundException { CommandAPDU selectFileApdu = new CommandAPDU(0x00, 0xA4, 0x08, 0x0C, fileId); ResponseAPDU responseApdu = transmit(selectFileApdu); if (0x9000 != responseApdu.getSW()) { throw new FileNotFoundException( "wrong status word after selecting file: " + Integer.toHexString(responseApdu.getSW())); } try { // SCARD_E_SHARING_VIOLATION fix Thread.sleep(20); } catch (InterruptedException e) { throw new RuntimeException("sleep error: " + e.getMessage()); } } private byte[] readBinary() throws CardException, IOException { int offset = 0; ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] data; do { CommandAPDU readBinaryApdu = new CommandAPDU(0x00, 0xB0, offset >> 8, offset & 0xFF, BLOCK_SIZE); ResponseAPDU responseApdu = transmit(readBinaryApdu); int sw = responseApdu.getSW(); if (0x6B00 == sw) { /* * Wrong parameters (offset outside the EF) End of file reached. * Can happen in case the file size is a multiple of 0xff bytes. */ break; } if (0x9000 != sw) { throw new IOException("APDU response error: " + responseApdu.getSW()); } /* * Introduce some delay for old Belpic V1 eID cards. */ // try { // Thread.sleep(50); // } catch (InterruptedException e) { // throw new RuntimeException("sleep error: " + e.getMessage(), e); // } data = responseApdu.getData(); baos.write(data); offset += data.length; } while (BLOCK_SIZE == data.length); return baos.toByteArray(); } private ResponseAPDU transmit(CommandAPDU commandApdu) throws CardException { ResponseAPDU responseApdu = this.cardChannel.transmit(commandApdu); if (0x6c == responseApdu.getSW1()) { /* * A minimum delay of 10 msec between the answer 6C xx and the * next APDU is mandatory for eID v1.0 and v1.1 cards. */ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException("cannot sleep"); } responseApdu = this.cardChannel.transmit(commandApdu); } return responseApdu; } public String resultAsString(Boolean full) { final Identity i = this.identity; final Address a = this.address; String s = "reader: BE"; s += "\nname: " + i.name; s += "\nfirstName: " + str2yaml(i.firstName); s += "\nmiddleName: " + str2yaml(i.middleName); s += "\nnationality: " + str2yaml(i.nationality); s += "\nplaceOfBirth: " + str2yaml(i.placeOfBirth); s += "\ndateOfBirth: " + cal2date(i.dateOfBirth); s += "\ngender: " + i.gender.toString(); s += "\nnationalNumber: " + i.nationalNumber; s += "\ncardNumber: " + i.cardNumber; s += "\nchipNumber: " + i.chipNumber; s += "\ncardDeliveryMunicipality: " + i.cardDeliveryMunicipality; s += "\nnobleCondition: " + str2yaml(i.nobleCondition); //~ s += "\ndocumentType: " + i.documentType.toString(); s += "\ndocumentType: " + Integer.toString(i.documentType.getKey()); s += "\nspecialStatus: " + i.specialStatus.toString(); s += "\nduplicate: " + str2yaml(i.duplicate); s += String.format("\nspecialOrganisation: %s", i.specialOrganisation); s += "\ncardValidityDateBegin: " + cal2date(i.cardValidityDateBegin); s += "\ncardValidityDateEnd: " + cal2date(i.cardValidityDateEnd); s += "\nstreetAndNumber: " + str2yaml(a.streetAndNumber); s += "\nzip: " + a.zip; s += "\nmunicipality: " + a.municipality; if (full) { if (this.photo.length > 0) { byte[] enc = Base64.encodeBase64(this.photo); s += "\nphoto: " + new String(enc); } } System.out.println(s); return s; } } public class EIDReader extends Applet { public static void main(String[] args) { System.out.println("EIDReader main()"); } public void init() { // System.err.println("Gonna disable the security manager..."); // System.setSecurityManager(null); // System.err.println("Security manager has been disabled "); System.err.println("EIDReader version 20140213 initialized"); } public void unused2_init() { System.err.println("Gonna set the security manager..."); //~ System.out.println("toto"); System.setSecurityManager(new SecurityManager() { @Override public void checkPermission(Permission permission) { if (permission instanceof CardPermission) { return; } //~ if (permission instanceof RuntimePermission) { //~ return; //~ } //~ if (permission instanceof FilePermission) { //~ return; //~ } java.security.AccessController.checkPermission(permission); } }); System.err.println("Initialized"); } public String readCard() { return readCard(true); } public String readCard(final Boolean full) { System.err.println("EIDReader.readCard()"); return AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { try { TerminalFactory factory = TerminalFactory.getDefault(); List<CardTerminal> terminals = factory.terminals().list(); if (terminals.size() == 0) { return "Error: No card reader found"; } //~ throws java.lang.IndexOutOfBoundsException when there is no reader CardTerminal terminal = terminals.get(0); if (!terminal.isCardPresent()) { return "Error: No card found on terminal"; //~ return new EidReaderResponse(new String[] { "No card found on terminal" }); } // "The best way to go is, as explained by Shane, to use the wildcard protocol." // https://forums.oracle.com/message/10531935 Card card = terminal.connect("T=0"); // Card card = terminal.connect("T=1"); // Card card = terminal.connect("*"); System.err.println("Protocol: " + card.getProtocol()); ATR atr = card.getATR(); CardChannel channel = card.getBasicChannel(); if (BelgianReader.matchesEidAtr(atr)) { System.err.println("It's a Belgian card"); BelgianReader pf = new BelgianReader(channel); return pf.resultAsString(full); } System.err.println("It's an Estonian card"); PersonalFile pf = new PersonalFile(channel); return pf.resultAsString(full); //~ return new String[] { pf.toString() }; //~ return new EidReaderResponse(pf.getData()); //~ return pf.getSurName(); } catch (Throwable e) { //~ return new EidReaderResponse(new String[] { e.toString() }); e.printStackTrace(); return "Error: " + e.toString(); } } }); } }