/*
* Copyright 2011 Licel LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.licel.jcardsim.base;
import com.licel.jcardsim.utils.AIDUtil;
import com.licel.jcardsim.utils.BiConsumer;
import javacard.framework.*;
import javacardx.apdu.ExtendedLength;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Base implementation of Java Card Runtime.
* @see JCSystem
* @see Applet
*/
public class SimulatorRuntime {
// holds the Applet registration callback
protected final ThreadLocal<BiConsumer<Applet,AID>> registrationCallback;
/** storage for installed applets */
protected final SortedMap<AID, ApplicationInstance> applets = new TreeMap<AID, ApplicationInstance>(AIDUtil.comparator());
/** storage for load files */
protected final SortedMap<AID, LoadFile> loadFiles = new TreeMap<AID, LoadFile>(AIDUtil.comparator());
/** storage for automatically generated loadFile AIDs */
protected final SortedMap<AID, AID> generatedLoadFileAIDs = new TreeMap<AID, AID>(AIDUtil.comparator());
/** method for resetting APDUs */
protected final Method apduPrivateResetMethod;
/** outbound response byte array buffer */
protected final byte[] responseBuffer = new byte[Short.MAX_VALUE + 2];
/** transient memory */
protected final TransientMemory transientMemory;
/** regular APDU */
protected final APDU shortAPDU;
/** extended APDU */
protected final APDU extendedAPDU;
/** current selected applet */
protected AID currentAID;
/** previous selected applet */
protected AID previousAID;
/** outbound response byte array buffer size */
protected short responseBufferSize = 0;
/** if the applet is currently being selected */
protected boolean selecting = false;
/** if extended APDUs are used */
protected boolean usingExtendedAPDUs = false;
/** current protocol */
protected byte currentProtocol = APDU.PROTOCOL_T0;
/** current depth of transaction */
protected byte transactionDepth = 0;
/** previousActiveObject */
protected Object previousActiveObject;
public SimulatorRuntime() {
this(new TransientMemory());
}
@SuppressWarnings("unchecked")
public SimulatorRuntime(TransientMemory transientMemory) {
this.transientMemory = transientMemory;
try {
Constructor<?> ctor = APDU.class.getDeclaredConstructors()[0];
ctor.setAccessible(true);
shortAPDU = (APDU) ctor.newInstance(false);
extendedAPDU = (APDU) ctor.newInstance(true);
apduPrivateResetMethod = APDU.class.getDeclaredMethod("internalReset", byte.class, ApduCase.class, byte[].class);
apduPrivateResetMethod.setAccessible(true);
Field f = Applet.class.getDeclaredField("registrationCallback");
f.setAccessible(true);
registrationCallback = (ThreadLocal<BiConsumer<Applet,AID>>) f.get(null);
} catch (Exception e) {
throw new RuntimeException("Internal reflection error", e);
}
}
/**
* Register <code>this</code> with <code>SimulatorRuntime</code>
*/
protected final void activateSimulatorRuntimeInstance() {
SimulatorSystem.setCurrentInstance(this);
}
/**
* @return current applet context AID or null
*/
public AID getAID() {
return currentAID;
}
/**
* Lookup applet by aid contains in byte array
* @param buffer the byte array containing the AID bytes
* @param offset the start of AID bytes in <code>buffer</code>
* @param length the length of the AID bytes in <code>buffer</code>
* @return Applet AID or null
*/
public AID lookupAID(byte buffer[], short offset, byte length) {
// no construct new AID, iterate applets
for (AID aid : applets.keySet()) {
if (aid.equals(buffer, offset, length)) {
return aid;
}
}
return null;
}
/**
* Lookup applet by aid
* @param lookupAid applet AID
* @return ApplicationInstance or null
*/
public ApplicationInstance lookupApplet(AID lookupAid) {
for (AID aid : applets.keySet()) {
if (aid.equals(lookupAid)) {
return applets.get(aid);
}
}
return null;
}
/**
* @return previous selected applet context AID or null
*/
public AID getPreviousContextAID() {
return previousAID;
}
/**
* Return <code>Applet</code> by it's AID or null
* @param aid applet <code>AID</code>
* @return Applet or null
*/
protected Applet getApplet(AID aid) {
if (aid == null) {
return null;
}
ApplicationInstance a = lookupApplet(aid);
if(a == null) return null;
else return a.getApplet();
}
/**
* Load applet
* @param aid Applet AID
* @param appletClass Applet class
*/
public void loadApplet(AID aid, Class<? extends Applet> appletClass) {
if (generatedLoadFileAIDs.keySet().contains(aid)) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
// generate a load file AID
byte[] generated = new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0, 0};
Util.setShort(generated, (short) 3, (short) generatedLoadFileAIDs.size());
AID generatedAID = AIDUtil.create(generated);
generatedLoadFileAIDs.put(aid, generatedAID);
loadLoadFile(new LoadFile(generatedAID, generatedAID, appletClass));
}
/**
* Load a LoadFile
* @param loadFile LoadFile to load
*/
public void loadLoadFile(LoadFile loadFile) {
AID key = loadFile.getAid();
if (loadFiles.keySet().contains(key) || applets.keySet().contains(key)) {
throw new IllegalStateException("LoadFile AID already used");
}
loadFiles.put(key, loadFile);
}
/**
* Delete applet
* @param aid Applet AID to delete
*/
protected void deleteApplet(AID aid) {
activateSimulatorRuntimeInstance();
ApplicationInstance applicationInstance = lookupApplet(aid);
if (applicationInstance == null) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
applets.remove(aid);
Applet applet = applicationInstance.getApplet();
if (applet == null) {
return;
}
if (getApplet(currentAID) == applet) {
deselect(applicationInstance);
}
if (applet instanceof AppletEvent) {
try {
((AppletEvent) applet).uninstall();
}
catch (Exception e) {
// ignore all
}
}
}
/**
* Check if applet is currently being selected
* @param aThis applet
* @return true if applet is being selected
*/
public boolean isAppletSelecting(Object aThis) {
return aThis == getApplet(getAID()) && selecting;
}
/**
* Transmit APDU to previous selected applet
* @param command command apdu
* @return response apdu
*/
public byte[] transmitCommand(byte[] command) throws SystemException {
activateSimulatorRuntimeInstance();
final ApduCase apduCase = ApduCase.getCase(command);
final byte[] theSW = new byte[2];
byte[] response;
Applet applet = getApplet(getAID());
selecting = false;
// check if there is an applet to be selected
if (!apduCase.isExtended() && isAppletSelectionApdu(command)) {
AID newAid = findAppletForSelectApdu(command, apduCase);
if (newAid != null) {
deselect(lookupApplet(getAID()));
currentAID = newAid;
applet = getApplet(getAID());
selecting = true;
}
else if (applet == null) {
Util.setShort(theSW, (short) 0, ISO7816.SW_APPLET_SELECT_FAILED);
return theSW;
}
}
if (applet == null) {
Util.setShort(theSW, (short) 0, ISO7816.SW_COMMAND_NOT_ALLOWED);
return theSW;
}
if (apduCase.isExtended()) {
if (applet instanceof ExtendedLength) {
usingExtendedAPDUs = true;
}
else {
Util.setShort(theSW, (short)0, ISO7816.SW_WRONG_LENGTH);
return theSW;
}
}
else {
usingExtendedAPDUs = false;
}
responseBufferSize = 0;
APDU apdu = getCurrentAPDU();
try {
if (selecting) {
boolean success;
try {
success = applet.select();
}
catch (Exception e) {
success = false;
}
if (!success) {
throw new ISOException(ISO7816.SW_APPLET_SELECT_FAILED);
}
}
// set apdu
resetAPDU(apdu, apduCase, command);
applet.process(apdu);
Util.setShort(theSW, (short) 0, (short) 0x9000);
} catch (Throwable e) {
Util.setShort(theSW, (short) 0, ISO7816.SW_UNKNOWN);
if (e instanceof CardException) {
Util.setShort(theSW, (short) 0, ((CardException) e).getReason());
} else if (e instanceof CardRuntimeException) {
Util.setShort(theSW, (short) 0, ((CardRuntimeException) e).getReason());
}
}
finally {
selecting = false;
resetAPDU(apdu, null, null);
}
// if theSW = 0x61XX or 0x9XYZ than return data (ISO7816-3)
if(theSW[0] == 0x61 || (theSW[0] >= (byte)0x90 && theSW[0] <= (byte)0x9F)) {
response = new byte[responseBufferSize + 2];
Util.arrayCopyNonAtomic(responseBuffer, (short) 0, response, (short) 0, responseBufferSize);
Util.arrayCopyNonAtomic(theSW, (short) 0, response, responseBufferSize, (short) 2);
}
else {
response = theSW;
}
return response;
}
protected AID findAppletForSelectApdu(byte[] selectApdu, ApduCase apduCase) {
if (apduCase == ApduCase.Case1 || apduCase == ApduCase.Case2) {
// on a regular Smartcard we would select the CardManager applet
// in this case we just select the first applet
return applets.isEmpty() ? null : applets.firstKey();
}
for (AID aid : applets.keySet()) {
if (aid.equals(selectApdu, ISO7816.OFFSET_CDATA, selectApdu[ISO7816.OFFSET_LC])) {
return aid;
}
}
for (AID aid : applets.keySet()) {
if (aid.partialEquals(selectApdu, ISO7816.OFFSET_CDATA, selectApdu[ISO7816.OFFSET_LC])) {
return aid;
}
}
return null;
}
protected void deselect(ApplicationInstance applicationInstance) {
activateSimulatorRuntimeInstance();
if (applicationInstance != null) {
try {
Applet applet = applicationInstance.getApplet();
applet.deselect();
} catch (Exception e) {
// ignore all
}
}
if (getTransactionDepth() != 0) {
abortTransaction();
}
transientMemory.clearOnDeselect();
}
/**
* Copy response bytes to internal buffer
* @param buffer source byte array
* @param bOff the starting offset in buffer
* @param len the length in bytes of the response
*/
public void sendAPDU(byte[] buffer, short bOff, short len) {
responseBufferSize = Util.arrayCopyNonAtomic(buffer, bOff, responseBuffer, responseBufferSize, len);
}
/**
* powerdown/powerup
*/
public void reset() {
Arrays.fill(responseBuffer, (byte) 0);
transactionDepth = 0;
responseBufferSize = 0;
currentAID = null;
previousAID = null;
transientMemory.clearOnReset();
}
public void resetRuntime() {
activateSimulatorRuntimeInstance();
Iterator<AID> aids = applets.keySet().iterator();
ArrayList<AID> aidsToTrash = new ArrayList<AID>();
while (aids.hasNext()) {
AID aid = aids.next();
aidsToTrash.add(aid);
}
for (AID anAidsToTrash : aidsToTrash) {
deleteApplet(anAidsToTrash);
}
loadFiles.clear();
generatedLoadFileAIDs.clear();
Arrays.fill(responseBuffer, (byte) 0);
transactionDepth = 0;
responseBufferSize = 0;
currentAID = null;
previousAID = null;
transientMemory.clearOnReset();
transientMemory.forgetBuffers();
}
public TransientMemory getTransientMemory() {
return transientMemory;
}
protected void resetAPDU(APDU apdu, ApduCase apduCase, byte[] buffer) {
try {
apduPrivateResetMethod.invoke(apdu, currentProtocol, apduCase, buffer);
} catch (Exception e) {
throw new RuntimeException("Internal reflection error", e);
}
}
public APDU getCurrentAPDU() {
return usingExtendedAPDUs ? extendedAPDU : shortAPDU;
}
/**
* Change protocol
* @param protocol protocol bits
* @see javacard.framework.APDU#getProtocol()
*/
public void changeProtocol(byte protocol) {
this.currentProtocol = protocol;
resetAPDU(shortAPDU, null, null);
resetAPDU(extendedAPDU, null, null);
}
public byte getAssignedChannel() {
return 0; // basic channel
}
/**
* @see javacard.framework.JCSystem#beginTransaction()
*/
public void beginTransaction() {
if (transactionDepth != 0) {
TransactionException.throwIt(TransactionException.IN_PROGRESS);
}
transactionDepth = 1;
}
/**
* @see javacard.framework.JCSystem#abortTransaction()
*/
public void abortTransaction() {
if (transactionDepth == 0) {
TransactionException.throwIt(TransactionException.NOT_IN_PROGRESS);
}
transactionDepth = 0;
}
/**
* @see javacard.framework.JCSystem#commitTransaction()
*/
public void commitTransaction() {
if (transactionDepth == 0) {
TransactionException.throwIt(TransactionException.NOT_IN_PROGRESS);
}
transactionDepth = 0;
}
/**
* @see javacard.framework.JCSystem#getTransactionDepth()
* @return 1 if transaction in progress, 0 if not
*/
public byte getTransactionDepth() {
return transactionDepth;
}
/**
* @see javacard.framework.JCSystem#getUnusedCommitCapacity()
* @return The current implementation always returns 32767
*/
public short getUnusedCommitCapacity() {
return Short.MAX_VALUE;
}
/**
* @see javacard.framework.JCSystem#getMaxCommitCapacity()
* @return The current implementation always returns 32767
*/
public short getMaxCommitCapacity() {
return Short.MAX_VALUE;
}
/**
* @see javacard.framework.JCSystem#getAvailableMemory(byte)
* @return The current implementation always returns 32767
*/
public short getAvailablePersistentMemory() {
return Short.MAX_VALUE;
}
/**
* @see javacard.framework.JCSystem#getAvailableMemory(byte)
* @return The current implementation always returns 32767
*/
public short getAvailableTransientResetMemory() {
return Short.MAX_VALUE;
}
/**
* @see javacard.framework.JCSystem#getAvailableMemory(byte)
* @return The current implementation always returns 32767
*/
public short getAvailableTransientDeselectMemory() {
return Short.MAX_VALUE;
}
/**
* @see javacard.framework.JCSystem#getAppletShareableInterfaceObject(javacard.framework.AID, byte)
* @param serverAID the AID of the server applet
* @param parameter optional parameter data
* @return the shareable interface object or <code>null</code>
*/
public Shareable getSharedObject(AID serverAID, byte parameter) {
Applet serverApplet = getApplet(serverAID);
if (serverApplet != null) {
return serverApplet.getShareableInterfaceObject(getAID(),
parameter);
}
return null;
}
/**
* @see javacard.framework.JCSystem#isObjectDeletionSupported()
* @return always false
*/
public boolean isObjectDeletionSupported() {
return false;
}
/**
* @see javacard.framework.JCSystem#requestObjectDeletion()
*/
public void requestObjectDeletion() {
if (!isObjectDeletionSupported()) {
throw new SystemException(SystemException.ILLEGAL_USE);
}
}
public void setJavaOwner(Object obj, Object owner) {}
public Object getJavaOwner(Object obj) {
return obj;
}
public short getJavaContext(Object obj) {
return 0;
}
public Object getPreviousActiveObject() {
return previousActiveObject;
}
public void setPreviousActiveObject(Object previousActiveObject) {
this.previousActiveObject = previousActiveObject;
}
protected static boolean isAppletSelectionApdu(byte[] apdu) {
final byte channelMask = (byte) 0xFC; // mask out %b000000xx
final byte p2Mask = (byte) 0xE3; // mask out %b000xxx00
final byte cla = (byte) (apdu[ISO7816.OFFSET_CLA] & channelMask);
final byte ins = apdu[ISO7816.OFFSET_INS];
final byte p1 = apdu[ISO7816.OFFSET_P1];
final byte p2 = (byte) (apdu[ISO7816.OFFSET_P2] & p2Mask);
return cla == ISO7816.CLA_ISO7816 && ins == ISO7816.INS_SELECT &&
p1 == 4 && p2 == 0;
}
public void installApplet(final AID appletAid, byte[] bArray, short bOffset, byte bLength) {
AID generatedAID = generatedLoadFileAIDs.get(appletAid);
if (generatedAID == null || !loadFiles.keySet().contains(generatedAID)) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
installApplet(generatedAID, generatedAID, appletAid, bArray, bOffset, bLength);
}
public void installApplet(AID loadFileAID, AID moduleAID, final AID appletAID,
byte[] bArray, short bOffset, byte bLength) {
activateSimulatorRuntimeInstance();
LoadFile loadFile = loadFiles.get(loadFileAID);
if (loadFile == null) {
throw new IllegalArgumentException("LoadFile AID not found " + AIDUtil.toString(loadFileAID));
}
Module module = loadFile.getModule(moduleAID);
if (module == null) {
throw new IllegalArgumentException("Module AID not found " + AIDUtil.toString(moduleAID));
}
Class<? extends Applet> appletClass = module.getAppletClass();
Method initMethod;
try {
initMethod = appletClass.getMethod("install",
new Class[]{byte[].class, short.class, byte.class});
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class does not provide install method");
}
final AtomicInteger callCount = new AtomicInteger(0);
registrationCallback.set(new BiConsumer<Applet,AID>() {
public void accept(Applet applet, AID installAID) {
// disallow second call to register
if (callCount.incrementAndGet() != 1) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
// register applet
if (installAID != null) {
applets.put(installAID, new ApplicationInstance(installAID, applet));
}
else {
applets.put(appletAID, new ApplicationInstance(appletAID, applet));
}
}
});
try {
initMethod.invoke(null, bArray, bOffset, bLength);
}
catch (SystemException e) {
throw e;
}
catch (Exception e) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
finally {
registrationCallback.set(null);
}
if (callCount.get() != 1) {
throw new SystemException(SystemException.ILLEGAL_AID);
}
}
/** Represents an Applet instance */
public static class ApplicationInstance {
private final AID aid;
private final Applet applet;
public ApplicationInstance(AID aid, Applet applet) {
this.aid = aid;
this.applet = applet;
}
public Applet getApplet(){
return applet;
}
@Override
public String toString() {
return String.format("ApplicationInstance (%s)", AIDUtil.toString(aid));
}
}
}