
jnasmartcardio.Smartcardio Maven / Gradle / Ivy
/*
* To the extent possible under law, contributors have waived all
* copyright and related or neighboring rights to work.
*/
package jnasmartcardio;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import javax.smartcardio.ATR;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardNotPresentException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactorySpi;
import jnasmartcardio.Winscard.Dword;
import jnasmartcardio.Winscard.DwordByReference;
import jnasmartcardio.Winscard.SCardReaderState;
import com.sun.jna.Platform;
import com.sun.jna.Structure;
public class Smartcardio extends Provider {
private static final long serialVersionUID = 1L;
static final int MAX_ATR_SIZE = 33;
public static final String PROVIDER_NAME = "JNA2PCSC";
public Smartcardio() {
super(PROVIDER_NAME, 0.0d, "JNA-to-PCSC Provider");
put("TerminalFactory.PC/SC", JnaTerminalFactorySpi.class.getName());
}
public static class JnaTerminalFactorySpi extends TerminalFactorySpi {
public static final int SCARD_SCOPE_USER = 0;
public static final int SCARD_SCOPE_TERMINAL = 1;
public static final int SCARD_SCOPE_SYSTEM = 2;
private final Winscard.WinscardLibInfo libInfo;
public JnaTerminalFactorySpi(Object parameter) {
this(Winscard.openLib());
}
public JnaTerminalFactorySpi(Winscard.WinscardLibInfo libInfo) {
this.libInfo = libInfo;
}
/**
* Likely exceptions
*
* - EstablishContextException(JnaPCSCException(
* {@link WinscardConstants#SCARD_E_NO_READERS_AVAILABLE})) the Daemon
* is not running (Windows 8, Linux). On Windows 8, the daemon is shut
* down when there are no readers plugged in. New PCSC versions (at
* least 1.7) also allow no new connections when there are no readers
* plugged in.
*
- EstablishContextException(JnaPCSCException(
* {@link WinscardConstants#SCARD_E_NO_SERVICE})) the Daemon is not
* running (OS X). On OS X (pcscd 1.4), the daemon is shut down when
* there are no readers plugged in, and the library gives this error.
* Can also happen on Windows when you don't have permission.
*
*/
@Override public CardTerminals engineTerminals() throws EstablishContextException {
Winscard.SCardContextByReference phContext = new Winscard.SCardContextByReference();
try {
check("SCardEstablishContext", libInfo.lib.SCardEstablishContext(new Dword(SCARD_SCOPE_SYSTEM), null, null, phContext));
} catch (JnaPCSCException e) {
throw new EstablishContextException(e);
}
Winscard.SCardContext scardContext = phContext.getValue();
return new JnaCardTerminals(libInfo, scardContext);
}
}
public static class JnaCardTerminals extends CardTerminals {
private final Winscard.SCardContext scardContext;
private final Winscard.WinscardLibInfo libInfo;
/** The readers that waitForChange observed in its last invocation. */
private final List knownReaders;
/**
* Readers that previously existed. Stored until the next
* {@link #waitForChange(long)} call.
*/
private final List zombieReaders;
private boolean knownReadersChanged;
/**
* Whether to use the PNP device to etect when new readers are plugged
* in. Unfortunately, this is now almost useless, because the smartcard
* service exits and gives errors when there are no readers.
*/
private final boolean usePnp = true;
private boolean isClosed;
public JnaCardTerminals(Winscard.WinscardLibInfo libInfo, Winscard.SCardContext scardContext) {
this.libInfo = libInfo;
this.scardContext = scardContext;
this.knownReaders = new ArrayList();
this.zombieReaders = new ArrayList();
if (usePnp) {
SCardReaderState pnpReaderState = new Winscard.SCardReaderState();
pnpReaderState.szReader = WinscardConstants.PNP_READER_ID;
knownReaders.add(pnpReaderState);
}
}
@Override public List list(State state) throws CardException {
if (null == state)
throw new NullPointerException("State must be non-null. To get all terminals, just call zero-arg list().");
if (state == State.CARD_REMOVAL || state == State.CARD_INSERTION) {
List r = new ArrayList();
for (int i = 0; i < knownReaders.size(); i++) {
SCardReaderState readerState = knownReaders.get(i);
if (usePnp && i == 0)
continue;
boolean wasPresent = 0 != (readerState.dwCurrentState.intValue() & WinscardConstants.SCARD_STATE_PRESENT);
boolean isPresent = 0 != (readerState.dwEventState.intValue() & WinscardConstants.SCARD_STATE_PRESENT);
int oldCounter = (readerState.dwCurrentState.intValue() >> 16) & 0xffff;
int newCounter = (readerState.dwEventState.intValue() >> 16) & 0xffff;
boolean cardInserted = ! wasPresent && isPresent ||
isPresent && oldCounter < newCounter ||
oldCounter + 1 < newCounter;
boolean cardRemoved = wasPresent && !isPresent ||
! isPresent && oldCounter < newCounter ||
oldCounter + 1 < newCounter;
boolean shouldAdd = state == State.CARD_INSERTION && cardInserted ||
state == State.CARD_REMOVAL && cardRemoved;
if (shouldAdd)
r.add(new JnaCardTerminal(libInfo, this, readerState.szReader));
}
if (state == State.CARD_REMOVAL) {
for (int i = 0; i < zombieReaders.size(); i++) {
SCardReaderState readerState = zombieReaders.get(i);
boolean wasPresent = 0 != (readerState.dwCurrentState.intValue() & WinscardConstants.SCARD_STATE_PRESENT);
if (wasPresent)
r.add(new JnaCardTerminal(libInfo, this, readerState.szReader));
}
}
return r;
}
List readerNames = listReaderNames();
if (readerNames.isEmpty())
return Collections.emptyList();
List filteredReaderNames;
if (state == State.ALL) {
filteredReaderNames = readerNames;
} else {
SCardReaderState[] readers = new SCardReaderState[readerNames.size()];
new SCardReaderState().toArray((Structure[])readers);
for (int i = 0; i < readers.length; i++) {
readers[i].szReader = readerNames.get(i);
}
check("SCardGetStatusChange", libInfo.lib.SCardGetStatusChange(scardContext, new Dword(0), readers, new Dword(readers.length)));
filteredReaderNames = new ArrayList();
boolean wantPresent = state == State.CARD_PRESENT;
for (int i = 0; i < readers.length; i++) {
boolean isPresent = 0 != (WinscardConstants.SCARD_STATE_PRESENT & readers[i].dwEventState.intValue());
if (wantPresent == isPresent)
filteredReaderNames.add(readers[i].szReader);
}
}
CardTerminal[] cardTerminals = new CardTerminal[filteredReaderNames.size()];
for (int i = 0; i < filteredReaderNames.size(); i++) {
String name = filteredReaderNames.get(i);
cardTerminals[i] = new JnaCardTerminal(libInfo, this, name);
}
return Collections.unmodifiableList(Arrays.asList(cardTerminals));
}
/** Simple wrapper around SCardListReaders. */
private List listReaderNames() throws JnaPCSCException {
DwordByReference pcchReaders = new DwordByReference();
byte[] mszReaders = null;
long err;
ByteBuffer mszReaderGroups = ByteBuffer.allocate("SCard$AllReaders".length() + 2);
mszReaderGroups.put("SCard$AllReaders".getBytes(Charset.forName("ascii")));
while (true) {
err = libInfo.lib.SCardListReaders(scardContext, mszReaderGroups, null, pcchReaders).longValue();
if (err != 0)
break;
mszReaders = new byte[pcchReaders.getValue().intValue()];
err = libInfo.lib.SCardListReaders(scardContext, mszReaderGroups, ByteBuffer.wrap(mszReaders), pcchReaders).longValue();
if ((int)err != WinscardConstants.SCARD_E_INSUFFICIENT_BUFFER)
break;
}
switch ((int)err) {
case SCARD_S_SUCCESS:
List readerNames = pcsc_multi2jstring(mszReaders);
return readerNames;
case SCARD_E_NO_READERS_AVAILABLE:
case SCARD_E_READER_UNAVAILABLE:
return Collections.emptyList();
default:
check("SCardListReaders", err);
throw new IllegalStateException();
}
}
/**
* Helper function for {@link #waitForChange(long)}. Lists the readers
* and updates 3 variables:
*
* - Any new readers are appended to {@link #knownReaders}.
*
- Any old readers are moved from {@link #knownReaders} to
* {@link #zombieReaders}.
*
- If any change is made, {@link #knownReadersChanged} is set so
* that the JNA array-of-struct can be reallocated.
*
*
* @return true if a reader was added or removed.
*/
private boolean updateKnownReaders() throws JnaPCSCException {
boolean isReaderAddedOrRemoved = false;
List currentReaderNames = listReaderNames();
HashSet existingReaderNames = new HashSet(knownReaders.size() - (usePnp?1:0));
Iterator it = knownReaders.iterator();
if (usePnp) it.next();
while (it.hasNext()) {
SCardReaderState reader = it.next();
existingReaderNames.add(reader.szReader);
if (currentReaderNames.contains(reader.szReader))
continue;
it.remove();
reader.dwEventState = new Dword(0);
zombieReaders.add(reader);
isReaderAddedOrRemoved = true;
}
List newReadersNames = new ArrayList();
for (String readerName: currentReaderNames) {
if (existingReaderNames.contains(readerName))
continue;
newReadersNames.add(readerName);
}
if (! newReadersNames.isEmpty()) {
SCardReaderState[] newReaders = new SCardReaderState[newReadersNames.size()];
new SCardReaderState().toArray((Structure[])newReaders);
for (int i = 0; i < newReaders.length; i++)
newReaders[i].szReader = newReadersNames.get(i);
check("SCardGetStatusChange", libInfo.lib.SCardGetStatusChange(scardContext, new Dword(0), newReaders, new Dword(newReaders.length)));
knownReaders.addAll(Arrays.asList(newReaders));
isReaderAddedOrRemoved = true;
}
if (isReaderAddedOrRemoved)
knownReadersChanged = true;
return isReaderAddedOrRemoved;
}
/**
* Block until any card is inserted or removed, or until the timeout.
*
*
* Deviation from the Sun version: the first
* {@link #waitForChange(long)} call always returns immediately. In
* Sun's version, if the card is inserted between your {@link #list()}
* call and the first {@link #waitForChange(long)} call, then your
* application can wait forever.
*
*
* Note: this method returns early when any smartcard state has changed
* (e.g. smartcard becomes in-use or idle). The caller cannot observe
* these changes though. So the caller should be able to handle changes
* that appear spurious.
*
*
* Likely exceptions
*
* - JnaPCSCException(
* {@link WinscardConstants#SCARD_E_SERVICE_STOPPED}) On Windows 8+, the
* service shuts down immediately when the last reader is unplugged.
* Then, you have to start polling because there is no daemon to
* subscribe to.
*
*/
@Override public boolean waitForChange(long timeoutMs) throws CardException {
if (timeoutMs < 0)
throw new IllegalArgumentException("Negative timeout " + timeoutMs);
else if (timeoutMs == 0)
timeoutMs = WinscardConstants.INFINITE;
zombieReaders.clear();
// On Linux pcsclite 1.7.4, the PNP reader does not return
// immediately when there is already a reader present that isn't in
// the array. Strangely, it works fine in OS X.
if (!usePnp || Platform.isLinux())
if (updateKnownReaders())
return true; // # of readers changed; return early.
if (knownReadersChanged) {
knownReadersChanged = false;
// allocate a contiguous array of struct, and copy
SCardReaderState[] arr = new SCardReaderState[knownReaders.size()];
new SCardReaderState().toArray((Structure[])arr);
for (int i = 0; i < knownReaders.size(); i++) {
SCardReaderState oldReader = knownReaders.get(i);
SCardReaderState newReader = arr[i];
newReader.szReader = oldReader.szReader;
newReader.dwCurrentState = oldReader.dwCurrentState;
newReader.dwEventState = oldReader.dwEventState;
newReader.cbAtr = oldReader.cbAtr;
System.arraycopy(oldReader.rgbAtr, 0, newReader.rgbAtr, 0, oldReader.cbAtr.intValue());
knownReaders.set(i, newReader);
}
}
for (SCardReaderState reader: knownReaders) {
reader.dwCurrentState = reader.dwEventState;
reader.dwEventState = new Dword(0);
}
SCardReaderState[] readers;
if (knownReaders.isEmpty()) {
// create array containing null, to avoid JNA exception:
// Structure array must have non-zero length
readers = new SCardReaderState[1];
} else {
readers = knownReaders.toArray(new SCardReaderState[knownReaders.size()]);
}
Dword statusError = libInfo.lib.SCardGetStatusChange(scardContext, new Dword(timeoutMs), readers, new Dword(readers.length));
if (WinscardConstants.SCARD_E_TIMEOUT == statusError.intValue())
return false;
else check("SCardGetStatusChange", statusError);
if (usePnp) {
boolean pnpChange = 0 != (knownReaders.get(0).dwEventState.intValue() & WinscardConstants.SCARD_STATE_CHANGED);
if (pnpChange)
updateKnownReaders();
}
return true;
}
@Override public String toString() {return String.format("%s{scardContext=%s}", getClass().getSimpleName(), scardContext);}
public void close() throws JnaPCSCException {
synchronized (this) {
if (isClosed) return;
else isClosed = true;
}
check("SCardReleaseContext", libInfo.lib.SCardReleaseContext(scardContext));
}
@Override public void finalize() throws JnaPCSCException {
close();
}
}
public static class JnaCardTerminal extends CardTerminal {
private final Winscard.WinscardLibInfo libInfo;
private final JnaCardTerminals cardTerminals;
private final String name;
public static final int SCARD_SHARE_EXCLUSIVE = 1;
public static final int SCARD_SHARE_SHARED = 2;
public static final int SCARD_SHARE_DIRECT = 3;
public static final int SCARD_PROTOCOL_T0 = 1;
public static final int SCARD_PROTOCOL_T1 = 2;
public static final int SCARD_PROTOCOL_RAW = 4;
public static final int SCARD_PROTOCOL_T15 = 8;
public static final int SCARD_PROTOCOL_ANY = SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1;
public static final int SCARD_UNKNOWN = 0x01;
public static final int SCARD_ABSENT = 0x02;
public static final int SCARD_PRESENT = 0x04;
public static final int SCARD_SWALLOWED = 0x08;
public static final int SCARD_POWERED = 0x10;
public static final int SCARD_NEGOTIABLE = 0x20;
public static final int SCARD_SPECIFIC = 0x40;
public JnaCardTerminal(Winscard.WinscardLibInfo libInfo, JnaCardTerminals cardTerminals, String name) {
this.libInfo = libInfo;
this.cardTerminals = cardTerminals;
this.name = name;
}
@Override public String getName() {return name;}
@Override public Card connect(String protocol) throws CardException {
int dwPreferredProtocols;
if ("T=0".equals(protocol)) dwPreferredProtocols = SCARD_PROTOCOL_T0;
else if ("T=1".equals(protocol)) dwPreferredProtocols = SCARD_PROTOCOL_T1;
else if ("*".equals(protocol)) dwPreferredProtocols = SCARD_PROTOCOL_ANY;
else if ("T=CL".equals(protocol)) dwPreferredProtocols = 0; // and SCARD_SHARE_DIRECT
else throw new IllegalArgumentException("Protocol should be one of T=0, T=1, *, T=CL. Got " + protocol);
Winscard.SCardHandleByReference phCard = new Winscard.SCardHandleByReference();
DwordByReference pdwActiveProtocol = new DwordByReference();
long err = libInfo.lib.SCardConnect(cardTerminals.scardContext, name, new Dword(SCARD_SHARE_SHARED), new Dword(dwPreferredProtocols), phCard, pdwActiveProtocol).longValue();
switch ((int)err) {
case SCARD_S_SUCCESS:
Winscard.SCardHandle scardHandle = phCard.getValue();
DwordByReference readerLength = new DwordByReference();
DwordByReference currentState = new DwordByReference();
DwordByReference currentProtocol = new DwordByReference();
ByteBuffer atrBuf = ByteBuffer.allocate(Smartcardio.MAX_ATR_SIZE);
DwordByReference atrLength = new DwordByReference(new Dword(Smartcardio.MAX_ATR_SIZE));
check("SCardStatus", libInfo.lib.SCardStatus(scardHandle, null, readerLength, currentState, currentProtocol, atrBuf, atrLength));
int readerLengthInt = readerLength.getValue().intValue();
ByteBuffer readerName = ByteBuffer.allocate(readerLengthInt);
check("SCardStatus", libInfo.lib.SCardStatus(scardHandle, readerName, readerLength, currentState, currentProtocol, atrBuf, atrLength).longValue());
int atrLengthInt = atrLength.getValue().intValue();
atrBuf.limit(atrLengthInt);
byte[] atrBytes = new byte[atrBuf.remaining()];
atrBuf.get(atrBytes);
ATR atr = new ATR(atrBytes);
int currentProtocolInt = currentProtocol.getValue().intValue();
return new JnaCard(libInfo, this, scardHandle, atr, currentProtocolInt);
case WinscardConstants.SCARD_W_REMOVED_CARD:
throw new JnaCardNotPresentException(err, "Card not present.");
default:
check("SCardConnect", err);
throw new RuntimeException("Should not reach here.");
}
}
@Override public boolean isCardPresent() throws CardException {
SCardReaderState[] rgReaderStates = new SCardReaderState[1];
new SCardReaderState().toArray((Structure[])rgReaderStates);
rgReaderStates[0].szReader = name;
SCardReaderState readerState = rgReaderStates[0];
check("SCardGetStatusChange", libInfo.lib.SCardGetStatusChange(cardTerminals.scardContext, new Dword(0), rgReaderStates, new Dword(rgReaderStates.length)));
return 0 != (readerState.dwEventState.intValue() & WinscardConstants.SCARD_STATE_PRESENT);
}
private boolean waitHelper(long timeoutMs, boolean cardPresent) throws JnaPCSCException {
if (timeoutMs < 0)
throw new IllegalArgumentException("Negative timeout " + timeoutMs);
if (timeoutMs == 0)
timeoutMs = WinscardConstants.INFINITE;
SCardReaderState[] rgReaderStates = new SCardReaderState[1];
new SCardReaderState().toArray((Structure[])rgReaderStates);
SCardReaderState readerState = rgReaderStates[0];
readerState.szReader = name;
int remainingTimeout = (int)timeoutMs;
while (cardPresent != (0 != (readerState.dwEventState.intValue() & WinscardConstants.SCARD_STATE_PRESENT))) {
long startTime = System.currentTimeMillis();
Dword err = libInfo.lib.SCardGetStatusChange(cardTerminals.scardContext, new Dword(remainingTimeout), rgReaderStates, new Dword(rgReaderStates.length));
long endTime = System.currentTimeMillis();
if (WinscardConstants.SCARD_E_TIMEOUT == err.intValue())
return false;
check("SCardGetStatusChange", err);
readerState.dwCurrentState = readerState.dwEventState;
readerState.dwEventState = new Dword(0);
if (remainingTimeout != WinscardConstants.INFINITE) {
if (remainingTimeout < endTime - startTime)
return false;
remainingTimeout -= endTime - startTime;
}
}
return true;
}
@Override public boolean waitForCardAbsent(long timeoutMs) throws CardException {
return waitHelper(timeoutMs, false);
}
@Override public boolean waitForCardPresent(long timeoutMs) throws CardException {
return waitHelper(timeoutMs, true);
}
@Override public String toString() {return String.format("%s{scardHandle=%s, name=%s}", getClass().getSimpleName(), cardTerminals.scardContext, name);}
}
public static class JnaCard extends Card {
private final Winscard.WinscardLibInfo libInfo;
@SuppressWarnings("unused") // prevent context from being finalized.
private final CardTerminal cardTerminal;
private final Winscard.SCardHandle scardHandle;
private final ATR atr;
/**
* One of {@link JnaCardTerminal#SCARD_PROTOCOL_RAW},
* {@link JnaCardTerminal#SCARD_PROTOCOL_T0},
* {@link JnaCardTerminal#SCARD_PROTOCOL_T1}
*/
private final int protocol;
public JnaCard(Winscard.WinscardLibInfo libInfo, JnaCardTerminal cardTerminal, Winscard.SCardHandle scardHandle, ATR atr, int protocol) {
this.libInfo = libInfo;
this.cardTerminal = cardTerminal;
this.scardHandle = scardHandle;
this.atr = atr;
this.protocol = protocol;
getProtocol(); // make sure it is valid.
}
@Override public void beginExclusive() throws CardException {
check("SCardBeginTransaction", libInfo.lib.SCardBeginTransaction(scardHandle));
}
public static final int SCARD_LEAVE_CARD = 0;
public static final int SCARD_RESET_CARD = 1;
public static final int SCARD_UNPOWER_CARD = 2;
public static final int SCARD_EJECT_CARD = 3;
@Override public void endExclusive() throws CardException {
check("SCardEndTransaction", libInfo.lib.SCardEndTransaction(scardHandle, new Dword(SCARD_LEAVE_CARD)));
// TODO: handle error SCARD_W_RESET_CARD esp. in Windows
}
@Override public void disconnect(boolean reset) throws CardException {
int dwDisposition = reset ? SCARD_RESET_CARD : SCARD_LEAVE_CARD;
check("SCardDisconnect", libInfo.lib.SCardDisconnect(scardHandle, new Dword(dwDisposition)));
}
@Override public ATR getATR() {return atr;}
@Override public String getProtocol() {
switch (protocol) {
case JnaCardTerminal.SCARD_PROTOCOL_T0: return "T=0";
case JnaCardTerminal.SCARD_PROTOCOL_T1: return "T=1";
case JnaCardTerminal.SCARD_PROTOCOL_RAW: return "T=CL"; // TODO: is this right?
}
throw new IllegalStateException("Unknown protocol: " + protocol);
}
@Override public JnaCardChannel getBasicChannel() {
return new JnaCardChannel(this, (byte)0);
}
/**
* Open a logical channel.
*
*
* Common exceptions:
*
* - JnaCardException(6200): processing warning
*
- JnaCardException(6881): logical channel not supported
*
- JnaCardException(6a81): function not supported
*
*/
@Override public CardChannel openLogicalChannel() throws CardException {
// manage channel: request a new logical channel from 0x01 to 0x13
JnaCardChannel basicChannel = getBasicChannel();
ResponseAPDU response = basicChannel.transmit(new CommandAPDU(0, 0x70, 0x00, 0x00, 1));
int sw = response.getSW();
if (0x9000 == sw) {
byte[] body = response.getData();
if (body.length == 1) {
int channel = 0xff & body[0];
if (channel == 0 || channel > 0x13)
throw new JnaCardException(sw, String.format("Expected manage channel response to contain channel number in 1-19; got %d", channel));
return new JnaCardChannel(this, channel);
} else {
throw new JnaCardException(sw, String.format("Expected body of length 1 in response to manage channel request; got %d", body.length));
}
} else {
throw new JnaCardException(sw, String.format("Error: sw=%04x in response to manage channel command.", sw));
}
}
/**
* @param controlCode
* one of the IOCTL_SMARTCARD_* constants from WinSmCrd.h
*/
@Override
public byte[] transmitControlCommand(int controlCode, byte[] arg1) throws CardException {
// there's no way from the API to know how big a receive buffer to use.
// Sun uses 8192 bytes, so we'll do the same.
ByteBuffer receiveBuf = ByteBuffer.allocate(8192);
DwordByReference lpBytesReturned = new DwordByReference();
ByteBuffer arg1Wrapped = ByteBuffer.wrap(arg1);
check("SCardControl", libInfo.lib.SCardControl(scardHandle, new Dword(controlCode), arg1Wrapped, new Dword(arg1.length), receiveBuf, new Dword(receiveBuf.remaining()), lpBytesReturned));
int bytesReturned = lpBytesReturned.getValue().intValue();
receiveBuf.limit(bytesReturned);
byte[] r = new byte[bytesReturned];
receiveBuf.get(r);
return r;
}
@Override public String toString() {return String.format("%s{scardHandle=%s}", getClass().getSimpleName(), scardHandle);}
}
public static class JnaCardChannel extends CardChannel {
private final JnaCard card;
private final int channel;
private boolean isClosed;
public JnaCardChannel(JnaCard card, int channel) {
this.card = card;
this.channel = channel;
}
@Override public void close() throws CardException {
if (isClosed)
return;
isClosed = true;
if (channel != 0) {
// manage channel: close
ByteBuffer command = ByteBuffer.wrap(new CommandAPDU(0, 0x70, 0x80, channel).getBytes());
ByteBuffer response = ByteBuffer.allocate(2);
transmitRaw(command, response);
response.rewind();
int sw = 0xffff & response.getShort();
if (sw != 0x9000) {
throw new JnaCardException(sw, "Could not close channel.");
}
} else {
// Mimick SUN with self-protection
throw new IllegalStateException("Basic channel can not be closed");
}
}
@Override public Card getCard() {return card;}
@Override public int getChannelNumber() {return channel;}
/**
* Transmit the command and return the result APDU.
*
*
* Note: currently, the response (including status bytes) is limited to
* 8192 bytes.
*
*
* The command sent to the card is the same as the given command, except
* that:
*
* - The class byte (CLA) is modified to contain the channel number.
* The secure messaging indication and command chaining control are not
* modified, but they must already be in the correct bits depending on
* the channel number!
*
- If T=0 and there is request data, then the Le byte is removed.
*
*
*
* Automatically handles sw=61xx (get response) and sw=6cxx (Le)
* responses by re-sending the appropriate request.
*/
@Override public ResponseAPDU transmit(CommandAPDU command) throws CardException {
if (command == null) {
throw new IllegalArgumentException("command is null");
}
byte[] commandCopy = command.getBytes();
ByteBuffer response = transmitImpl(commandCopy, null);
ResponseAPDU responseApdu = convertResponse(response);
return responseApdu;
}
/**
* Transmit the given command APDU and store the response APDU. Returns
* the length of the response APDU.
*
*
* The response (including status bytes) must fit within the given
* response buffer.
*
*
* The command sent to the card is the same as the given command, except
* that:
*
* - The class byte (CLA) is modified to contain the channel number.
* The secure messaging indication and command chaining control are not
* modified, but they must already be in the correct bits depending on
* the channel number!
*
- If T=0 and there is request data, then the Le byte is removed.
*
*
*
* Automatically handles sw=61xx (get response) and sw=6cxx (Le)
* responses by re-sending the appropriate request.
*/
@Override public int transmit(ByteBuffer command, ByteBuffer response) throws CardException {
if (command == null) {
throw new IllegalArgumentException("command is null");
}
if (response == null) {
throw new IllegalArgumentException("response is null");
}
byte[] commandCopy = new byte[command.remaining()];
command.get(commandCopy);
int startPosition = response.position();
transmitImpl(commandCopy, response);
int endPosition = response.position();
return endPosition - startPosition;
}
private boolean isExtendedApdu(byte[] commandApdu) {
return commandApdu.length >= 7 && commandApdu[4] == 0;
}
/**
* Set the CLA byte, transmit the command, send Get Response commands as
* needed, and return the response ByteBuffer.
*
*
* The command is modified as is convenient, since it is assumed to
* already be a copy.
*
*
* Reminder: there are several forms of APDU:
* 1. CLA INS P1 P2. No body, no response body.
* 2s. CLA INS P1 P2 Le. No body. Le in [1,00 (256)]
* 2e. CLA INS P1 P2 00 Le1 Le2. No body. Le in [1,0000 (65536)]
* T=0: use Le = 00
* 3s. CLA INS P1 P2 Lc <body>. No response. Lc in [1,255]
* 3e. CLA INS P1 P2 00 Lc1 Lc2 <body>. No response. Lc in [1,ffff]
* T=0: if Nc <= 255, then use short form. Else, use envelope.
* 4s. CLA INS P1 P2 Lc <body> Le. No response. Lc in [1,00]. Le in [1,ff]
* 4e. CLA INS P1 P2 00 Lc1 Lc2 <body> 00 Le1 Le2. Lc in [1,0000]. Le in [1,ffff]
*
*
* This method handles:
*
* - Set the channel number bits in the class byte.
*
- If T=0, then convert APDU to T=0 TPDU (ISO 7816-3). In
* particular, if T=0 and there is request data, then strip the Le field
*
- If sw = 61xx, then call c0 get response and concatenate
*
- If sw = 6cxx, then retransmit with Le = xx
*
* Q: Should it also handle
*
* - Command chaining (if mentioned in historic bytes) (bit 5 of cla =
* true)
*
- Envelope (ins = c2 or c3)
*
*
*
* T=0 protocol: 3 cases
*
* - CLA INS P1 P2. Response will always be 2 bytes.
*
- CLA INS P1 P2 Le. Response will be up to Le+2 bytes. (Le=0 means
* 256 bytes). Use get response commands to get all the data.
*
- CLA INS P1 P2 Lc. <outgoing data>. Response will always be 2
* bytes. Long commands need to be enclosed in envelope commands.
*
*
*
* T=1 protocol: CLA INS P1 P2 [Lc Data] [Le].
* Lc is either 01-ff or 000001-00ffff.
* Le is either 00-ff (00=256) or 0000-ffff. (0000=65536)
*/
private ByteBuffer transmitImpl(byte[] command, ByteBuffer response) throws CardException, JnaPCSCException {
// Mimic SUN with self-defense
if (card.protocol == JnaCardTerminal.SCARD_PROTOCOL_T0 && isExtendedApdu(command))
throw new CardException("Extended APDU requires T=1");
command[0] = getClassByte(command[0], getChannelNumber());
ByteBuffer commandBuffer = ByteBuffer.wrap(command);
// Allocate memory if not given: 8K
if (response == null)
response = ByteBuffer.allocate(8192);
// TODO: implement compatibility with SUN properties
// Don't loop forever.
for (int i=0; i<8; i++) {
int posBeforeTransmit = response.position();
transmitRaw(commandBuffer, response);
// Roll back to read SW
response.position(response.position() - 2);
byte sw1 = response.get();
byte sw2 = response.get();
if (0x6c == sw1) {
command[command.length - 1] = sw2;
response.position(posBeforeTransmit);
commandBuffer.rewind();
} else if (0x61 == sw1) {
// send Get Response command.
// Don't touch CLA as per 7816-4
command[1] = (byte) 0xc0;
command[2] = (byte) 0x00;
command[3] = (byte) 0x00;
command[4] = sw2;
commandBuffer.position(0);
commandBuffer.limit(5);
// concatenate new response to the same buffer.
// Roll back to overwrite current SW.
response.position(response.position() - 2);
} else {
break;
}
}
return response;
}
/**
* Set the channel number on the class byte. Does not touch the command
* chaining control or secure messaging indication, but they must be in
* the correct bits for this channel number.
*/
static byte getClassByte(byte origCla, int channelNumber) {
if ((0x80 & origCla) != 0) {
// Not an interindustry class; don't touch it.
return origCla;
}
int cla;
// 7816-4/2005 5.1.1 Class byte
if (0 <= channelNumber && channelNumber <= 3) {
// First interindustry values of CLA
// Class byte is 000x xxcc, where cc is channel number
cla = (origCla & 0x1c) | channelNumber;
} else if (0x04 <= channelNumber && channelNumber <= 0x13) {
// Further interindustry values of CLA
// Class byte is 01xx cccc, where cccc is channel number - 4.
int channelBits = channelNumber - 4;
cla = (origCla & 0x30) | channelBits | 0x40;
} else {
throw new IllegalStateException("Bad channel number; expected 0-19; got " + channelNumber);
}
return (byte) cla;
}
private static ResponseAPDU convertResponse(ByteBuffer responseBuf) {
byte[] responseBytes = new byte[responseBuf.position()];
responseBuf.rewind();
responseBuf.get(responseBytes);
return new ResponseAPDU(responseBytes);
}
/**
* Transmit the given apdu. On success, the command buffer is advanced
* to its limit, and the response buffer is advanced by the number of
* bytes received from the card.
*/
private int transmitRaw(ByteBuffer command, ByteBuffer response) throws JnaPCSCException {
Winscard.ScardIoRequest pioSendPci;
switch (card.protocol) {
case JnaCardTerminal.SCARD_PROTOCOL_T0:
pioSendPci = card.libInfo.SCARD_PCI_T0;
break;
case JnaCardTerminal.SCARD_PROTOCOL_T1:
pioSendPci = card.libInfo.SCARD_PCI_T1;
break;
case JnaCardTerminal.SCARD_PROTOCOL_RAW:
pioSendPci = card.libInfo.SCARD_PCI_RAW;
break;
default:
throw new IllegalStateException("Don't know how to transmit for protocol " + card.protocol);
}
DwordByReference recvLength = new DwordByReference(new Dword(response.remaining()));
check("SCardTransmit", card.libInfo.lib.SCardTransmit(card.scardHandle, pioSendPci, command, new Dword(command.remaining()), null, response, recvLength));
int recvLengthInt = recvLength.getValue().intValue();
assert recvLengthInt >= 0;
command.position(command.limit());
int newPosition = response.position() + recvLengthInt;
response.position(newPosition);
return recvLengthInt;
}
@Override public String toString() {return String.format("%s{card=%s, channel=%d}", getClass().getSimpleName(), this.card, this.channel);}
}
public static class JnaPCSCException extends CardException {
private static final long serialVersionUID = 1L;
public final long code;
public JnaPCSCException(String message) {this(0, message, null);}
public JnaPCSCException(Throwable cause) {this(0, null, cause);}
public JnaPCSCException(long code, String message) {this(code, message, null);}
public JnaPCSCException(long code, String message, Throwable cause) {super(message, cause); this.code = code;}
}
/**
* Wrapper for {@link JnaPCSCException} because TerminalFactory.terminals()
* is not allowed to throw checked exceptions.
*/
public static class EstablishContextException extends RuntimeException {
private static final long serialVersionUID = 1L;
public EstablishContextException(JnaPCSCException cause) {super(cause);}
/** Overridden with more specific return type so you don't have to cast,*/
@Override public JnaPCSCException getCause() {return (JnaPCSCException) super.getCause();}
}
public static class JnaCardNotPresentException extends CardNotPresentException {
private static final long serialVersionUID = 1L;
public final long code;
public JnaCardNotPresentException(long code, String message) {super(message); this.code = code;}
}
public static class JnaCardException extends CardException {
private static final long serialVersionUID = 1L;
public final int sw;
public JnaCardException(int sw, String message) {this(sw, message, null);}
public JnaCardException(int sw, String message, Throwable cause) {super(message, cause); this.sw = sw;}
}
public static final int SCARD_S_SUCCESS = 0x0;
public static final int SCARD_E_NO_READERS_AVAILABLE = 0x8010002E;
public static final int SCARD_E_READER_UNAVAILABLE = 0x80100017;
public static final int SCARD_E_NO_SMARTCARD = 0x8010000C;
/**
* Named affectionately after the function I've seen in crash logs so often
* from libj2pcsc on OS X java7.
* @param
*/
private static List pcsc_multi2jstring(byte[] multiString, Charset charset) {
List r = new ArrayList();
int from = 0, to = 0;
for (; to < multiString.length; to++) {
if (multiString[to] != '\0')
continue;
if (from == to)
return r;
byte[] bytes = Arrays.copyOfRange(multiString, from, to);
r.add(new String(bytes, charset));
from = to + 1;
}
throw new IllegalArgumentException("Multistring must be end with a null-terminated empty string.");
}
private static List pcsc_multi2jstring(byte[] multiString) {
return pcsc_multi2jstring(multiString, Charset.forName("UTF-8"));
}
private static void check(String message, Dword code) throws JnaPCSCException {
check(message, code.longValue());
}
private static void check(String message, long code) throws JnaPCSCException {
if (code == 0)
return;
int icode = (int)code;
String codeName = WinscardConstants.ERROR_TO_VARIABLE_NAME.get(icode);
String codeDescription = WinscardConstants.ERROR_TO_DESCRIPTION.get(icode);
throw new JnaPCSCException(code, String.format("%s got response 0x%x (%s: %s)", message, icode, codeName, codeDescription));
}
}