/**************************************************************************** * Copyright (C) 2012 ecsec GmbH. * All rights reserved. * Contact: ecsec GmbH (info@ecsec.de) * * This file is part of the Open eCard App. * * GNU General Public License Usage * This file may be used under the terms of the GNU General Public * License version 3.0 as published by the Free Software Foundation * and appearing in the file LICENSE.GPL included in the packaging of * this file. Please review the following information to ensure the * GNU General Public License version 3.0 requirements will be met: * http://www.gnu.org/copyleft/gpl.html. * * Other Usage * Alternatively, this file may be used in accordance with the terms * and conditions contained in a signed written agreement between * you and ecsec GmbH. * ***************************************************************************/ package org.openecard.recognition; import iso.std.iso_iec._24727.tech.schema.BeginTransaction; import iso.std.iso_iec._24727.tech.schema.BeginTransactionResponse; import iso.std.iso_iec._24727.tech.schema.CardCall; import iso.std.iso_iec._24727.tech.schema.CardInfoType; import iso.std.iso_iec._24727.tech.schema.Connect; import iso.std.iso_iec._24727.tech.schema.ConnectResponse; import iso.std.iso_iec._24727.tech.schema.ConnectionHandleType.RecognitionInfo; import iso.std.iso_iec._24727.tech.schema.DataMaskType; import iso.std.iso_iec._24727.tech.schema.Disconnect; import iso.std.iso_iec._24727.tech.schema.DisconnectResponse; import iso.std.iso_iec._24727.tech.schema.EndTransaction; import iso.std.iso_iec._24727.tech.schema.EndTransactionResponse; import iso.std.iso_iec._24727.tech.schema.GetCardInfoOrACD; import iso.std.iso_iec._24727.tech.schema.GetCardInfoOrACDResponse; import iso.std.iso_iec._24727.tech.schema.GetRecognitionTreeResponse; import iso.std.iso_iec._24727.tech.schema.InputAPDUInfoType; import iso.std.iso_iec._24727.tech.schema.MatchingDataType; import iso.std.iso_iec._24727.tech.schema.RecognitionTree; import iso.std.iso_iec._24727.tech.schema.ResponseAPDUType; import iso.std.iso_iec._24727.tech.schema.Transmit; import iso.std.iso_iec._24727.tech.schema.TransmitResponse; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.TreeMap; import oasis.names.tc.dss._1_0.core.schema.InternationalStringType; import oasis.names.tc.dss._1_0.core.schema.Result; import org.openecard.common.ECardConstants; import org.openecard.common.I18n; import org.openecard.common.apdu.common.CardResponseAPDU; import org.openecard.common.tlv.TLV; import org.openecard.common.tlv.TLVException; import org.openecard.common.util.ByteUtils; import org.openecard.common.util.FileUtils; import org.openecard.gui.MessageDialog; import org.openecard.gui.UserConsent; import org.openecard.gui.message.DialogType; import org.openecard.recognition.staticrepo.LocalCifRepo; import org.openecard.recognition.statictree.LocalFileTree; import org.openecard.ws.GetRecognitionTree; import org.openecard.ws.IFD; import org.openecard.ws.marshal.WSMarshaller; import org.openecard.ws.marshal.WSMarshallerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Tobias Wich <tobias.wich@ecsec.de> */ public class CardRecognition { private static final Logger _logger = LoggerFactory.getLogger(CardRecognition.class); private final I18n lang = I18n.getTranslation("recognition"); private final RecognitionTree tree; private final org.openecard.ws.GetCardInfoOrACD cifRepo; private final TreeMap<String, CardInfoType> cifCache = new TreeMap<String, CardInfoType>(); private final Properties cardImagesMap = new Properties(); private final IFD ifd; private final byte[] ctx; private UserConsent gui; /** * Create recognizer with tree from local (file based) repository. * @param ifd * @param ctx * @throws Exception */ public CardRecognition(IFD ifd, byte[] ctx) throws Exception { this(ifd, ctx, null, null); } public CardRecognition(IFD ifd, byte[] ctx, GetRecognitionTree treeRepo, org.openecard.ws.GetCardInfoOrACD cifRepo) throws Exception { this.ifd = ifd; this.ctx = ByteUtils.clone(ctx); // load alternative tree service if needed WSMarshaller marshaller = WSMarshallerFactory.createInstance(); if (treeRepo == null) { treeRepo = new LocalFileTree(marshaller); } if (cifRepo == null) { cifRepo = new LocalCifRepo(marshaller); } this.cifRepo = cifRepo; cardImagesMap.load(FileUtils.resolveResourceAsStream(CardRecognition.class, "/card-images/card-images.properties")); // request tree from service iso.std.iso_iec._24727.tech.schema.GetRecognitionTree req = new iso.std.iso_iec._24727.tech.schema.GetRecognitionTree(); req.setAction(RecognitionProperties.getAction()); GetRecognitionTreeResponse resp = treeRepo.getRecognitionTree(req); checkResult(resp.getResult()); this.tree = resp.getRecognitionTree(); } public void setGUI(UserConsent gui) { this.gui = gui; } public List<CardInfoType> getCardInfos() { // TODO: add caching GetCardInfoOrACD req = new GetCardInfoOrACD(); req.setAction(ECardConstants.CIF.GET_OTHER); GetCardInfoOrACDResponse res = cifRepo.getCardInfoOrACD(req); // checkout response if it contains our cardinfo List<Object> cifs = res.getCardInfoOrCapabilityInfo(); ArrayList<CardInfoType> result = new ArrayList<CardInfoType>(); for (Object next : cifs) { if (next instanceof CardInfoType) { result.add((CardInfoType) next); } } return result; } public CardInfoType getCardInfo(String type) { // only do something when a repo is specified if (cifRepo != null) { if (cifCache.containsKey(type)) { return cifCache.get(type); } else { GetCardInfoOrACD req = new GetCardInfoOrACD(); req.setAction(ECardConstants.CIF.GET_SPECIFIED); req.getCardTypeIdentifier().add(type); GetCardInfoOrACDResponse res = cifRepo.getCardInfoOrACD(req); // checkout response if it contains our cardinfo List<Object> cifs = res.getCardInfoOrCapabilityInfo(); for (Object next : cifs) { if (next instanceof CardInfoType) { cifCache.put(type, (CardInfoType) next); return (CardInfoType) next; } } // no valid cardinfo save null to the map to prevent fetching the nonexistant cif again cifCache.put(type, null); return null; } } else { return null; } } /** * Gets the translated card name for a card type. * * @param cardType The card type to get the card name for. * @return A card name matching the users locale or the English name as default. If the card is not supported, the * string {@code Unknown card type} is returned. */ public String getTranslatedCardName(String cardType) { CardInfoType info = getCardInfo(cardType); Locale userLocale = Locale.getDefault(); String langCode = userLocale.getLanguage(); String enFallback = "Unknown card type."; if (info == null) { // we can identify the card but do not have a card info file for it return enFallback; } for (InternationalStringType typ : info.getCardType().getCardTypeName()) { if (typ.getLang().equalsIgnoreCase("en")) { enFallback = typ.getValue(); } if (typ.getLang().equalsIgnoreCase(langCode)) { return typ.getValue(); } } return enFallback; } /** * Gets image stream of the given card or a no card image if the object identifier is unknown. * @param objectid iso:ObjectIdentifier as defined in the CardInfo file. * @return InputStream of the card image. */ public InputStream getCardImage(String objectid) { String fname = cardImagesMap.getProperty(objectid); InputStream fs = null; if (fname != null) { fs = loadCardImage(fname); } if (fs == null) { fs = getUnknownCardImage(); } return fs; } /** * @see #getCardImage(java.lang.String) */ public InputStream getUnknownCardImage() { return loadCardImage("unknown_card.png"); } /** * @see #getCardImage(java.lang.String) */ public InputStream getNoCardImage() { return loadCardImage("no_card.jpg"); } /** * @see #getCardImage(java.lang.String) */ public InputStream getNoTerminalImage() { return loadCardImage("no_terminal.png"); } /** * Gets stream of the given image in the directory card-images. * @param filename * @return Stream of the image or null, if none is found. */ private static InputStream loadCardImage(String filename) { try { return FileUtils.resolveResourceAsStream(CardRecognition.class, "/card-images/" + filename); } catch (IOException ex) { _logger.info("Failed to load card image '" + filename + "'.", ex); return null; } } public RecognitionInfo recognizeCard(String ifdName, BigInteger slot) throws RecognitionException { // connect card byte[] slotHandle = connect(ifdName, slot); // recognise card String type = treeCalls(slotHandle, tree.getCardCall()); // disconnect and return disconnect(slotHandle); // build result or throw exception if it is null if (type == null) { return null; } RecognitionInfo info = new RecognitionInfo(); info.setCardType(type); return info; } private void checkResult(Result r) throws RecognitionException { if (r.getResultMajor().equals(ECardConstants.Major.ERROR)) { throw new RecognitionException(r); } } /** * Special transmit check determining only whether a response is present or not and it contains at least a trailer.<br/> * Unexpected result may be the wrong cause, because the command could represent multiple commands. * * @param r The response to check * @return True when result present, false otherwise. */ private boolean checkTransmitResult(TransmitResponse r) { if (! r.getOutputAPDU().isEmpty() && r.getOutputAPDU().get(0).length >= 2) { return true; } else { return false; } } /** * Returns the fibonacci number for a given index. * * @param idx index * @return the fibonacci number for the given index */ private long fibonacci(int idx) { if (idx == 1 || idx == 2) { return 1; } else { return fibonacci(idx - 1) + fibonacci(idx - 2); } } private byte[] connect(String ifdName, BigInteger slot) throws RecognitionException { Connect c = new Connect(); c.setContextHandle(ctx); c.setIFDName(ifdName); c.setSlot(slot); ConnectResponse r = ifd.connect(c); checkResult(r.getResult()); waitForExclusiveCardAccess(r.getSlotHandle(), ifdName); return r.getSlotHandle(); } /** * This method tries to get exclusive card access until it is granted. * The waiting delay between the attempts is determined by the fibonacci numbers. * * @param slotHandle slot handle specifying the card to get exclusive access for * @param ifdName Name of the IFD in which the card is inserted */ private void waitForExclusiveCardAccess(byte[] slotHandle, String ifdName) { String resultMajor; int i = 2; do { // try to get exclusive card access for the recognition run BeginTransaction trans = new BeginTransaction(); trans.setSlotHandle(slotHandle); BeginTransactionResponse resp = ifd.beginTransaction(trans); resultMajor = resp.getResult().getResultMajor(); if (! resultMajor.equals(ECardConstants.Major.OK)) { // could not get exclusive card access, wait in increasingly longer intervals and retry try { long waitInSeconds = fibonacci(i); i++; _logger.debug("Could not get exclusive card access. Trying again in {} seconds.", waitInSeconds); if (i == 6 && gui != null) { MessageDialog dialog = gui.obtainMessageDialog(); String message = lang.translationForKey("message", ifdName); String title = lang.translationForKey("error", ifdName); dialog.showMessageDialog(message, title, DialogType.WARNING_MESSAGE); } Thread.sleep(1000 * waitInSeconds); } catch (InterruptedException e) { // ignore } } } while (! resultMajor.equals(ECardConstants.Major.OK)); } private void disconnect(byte[] slotHandle) throws RecognitionException { // end exclusive card access EndTransaction end = new EndTransaction(); end.setSlotHandle(slotHandle); EndTransactionResponse endTransactionResponse = ifd.endTransaction(end); checkResult(endTransactionResponse.getResult()); Disconnect c = new Disconnect(); c.setSlotHandle(slotHandle); DisconnectResponse r = ifd.disconnect(c); checkResult(r.getResult()); } private byte[] transmit(byte[] slotHandle, byte[] input, List<ResponseAPDUType> results) { Transmit t = new Transmit(); t.setSlotHandle(slotHandle); InputAPDUInfoType apdu = new InputAPDUInfoType(); apdu.setInputAPDU(input); for (ResponseAPDUType result : results) { apdu.getAcceptableStatusCode().add(result.getTrailer()); } t.getInputAPDUInfo().add(apdu); TransmitResponse r = ifd.transmit(t); if (checkTransmitResult(r)) { return r.getOutputAPDU().get(0); } else { return null; } } private List<CardCall> branch2list(CardCall first) { LinkedList<CardCall> calls = new LinkedList<CardCall>(); calls.add(first); CardCall next = first; // while next is a select call while (next.getResponseAPDU().get(0).getBody() == null) { // a select only has one call in its conclusion next = next.getResponseAPDU().get(0).getConclusion().getCardCall().get(0); calls.add(next); } return calls; } private String treeCalls(byte[] slotHandle, List<CardCall> calls) throws RecognitionException { for (CardCall c : calls) { // make list of next feature (aka branch) List<CardCall> branch = branch2list(c); // execute selects and then matcher, matcher decides over success for (CardCall next : branch) { boolean matcher = (next.getResponseAPDU().get(0).getBody() != null) ? true : false; byte[] resultBytes = transmit(slotHandle, next.getCommandAPDU(), next.getResponseAPDU()); // break when outcome is wrong if (resultBytes == null) { break; } // get command bytes and trailer byte[] result = CardResponseAPDU.getData(resultBytes); byte[] trailer = CardResponseAPDU.getTrailer(resultBytes); // if select, only one response exists if (!matcher && ! Arrays.equals(next.getResponseAPDU().get(0).getTrailer(), trailer)) { // break when outcome is wrong break; } else if (!matcher) { // trailer matches expected response from select, continue continue; } else { // matcher command, loop through responses for (ResponseAPDUType r : next.getResponseAPDU()) { // next response, when outcome is wrong if (! Arrays.equals(r.getTrailer(), trailer)) { continue; } // check internals for match if (checkDataObject(r.getBody(), result)) { if (r.getConclusion().getRecognizedCardType() != null) { // type recognised return r.getConclusion().getRecognizedCardType(); } else { // type dependent on subtree return treeCalls(slotHandle, r.getConclusion().getCardCall()); } } } } } } return null; } private boolean checkDataObject(DataMaskType matcher, byte[] result) { // check if we have a tag and data object if (matcher.getTag() != null && matcher.getDataObject() != null) { try { TLV tlv = TLV.fromBER(result); return checkDataObject(matcher, tlv); } catch (TLVException ex) { } // no TLV structure or fallthrough after tag not found return false; } // we have a matcher return checkMatchingData(matcher.getMatchingData(), result); } private boolean checkDataObject(DataMaskType matcher, TLV result) { byte[] tag = matcher.getTag(); DataMaskType nextMatcher = matcher.getDataObject(); // this function only works with tag and dataobject if (tag == null || nextMatcher == null) { return false; } long tagNum = ByteUtils.toLong(tag); List<TLV> chunks = result.findNextTags(tagNum); for (TLV next : chunks) { boolean outcome; if (nextMatcher.getMatchingData() != null) { outcome = checkMatchingData(nextMatcher.getMatchingData(), next.getValue()); } else { outcome = checkDataObject(nextMatcher, next.getChild()); } // evaluate outcome if (outcome == true) { return true; } } // no match return false; } private boolean checkMatchingData(MatchingDataType matcher, byte[] result) { // get values byte[] offsetBytes = matcher.getOffset(); byte[] lengthBytes = matcher.getLength(); byte[] valueBytes = matcher.getMatchingValue(); byte[] maskBytes = matcher.getMask(); // convert values for convenience if (offsetBytes == null) { offsetBytes = new byte[] {(byte) 0x00, (byte) 0x00}; } int offset = ByteUtils.toInteger(offsetBytes); int length = ByteUtils.toInteger(lengthBytes); if (maskBytes == null) { maskBytes = new byte[valueBytes.length]; for (int i = 0; i < maskBytes.length; i++) { maskBytes[i] = (byte) 0xFF; } } // some basic integrity checks if (maskBytes.length != valueBytes.length) { return false; } if (valueBytes.length != length) { return false; } if (result.length < length + offset) { return false; } // check for (int i = offset; i < length + offset; i++) { if ((maskBytes[i - offset] & result[i]) != valueBytes[i - offset]) { return false; } } return true; } }