/*
* Commons eID Project.
* Copyright (C) 2008-2013 FedICT.
* Copyright (C) 2015-2016 e-Contract.be BVBA.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
package be.fedict.commons.eid.client;
import java.awt.GraphicsEnvironment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import javax.smartcardio.ATR;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import be.fedict.commons.eid.client.event.BeIDCardListener;
import be.fedict.commons.eid.client.impl.BeIDDigest;
import be.fedict.commons.eid.client.impl.CCID;
import be.fedict.commons.eid.client.impl.LocaleManager;
import be.fedict.commons.eid.client.impl.VoidLogger;
import be.fedict.commons.eid.client.spi.BeIDCardUI;
import be.fedict.commons.eid.client.spi.Logger;
import be.fedict.commons.eid.client.spi.UserCancelledException;
/**
* One BeIDCard instance represents one Belgian Electronic Identity Card,
* physically present in a connected javax.smartcardio.CardTerminal. It exposes
* the publicly accessible features of the BELPIC applet on the card's chip:
* <ul>
* <li>Reading Certificates and Certificate Chains
* <li>Signing of digests non-repudiation and authentication purposes
* <li>Verification and Alteration of the PIN code
* <li>Reading random bytes from the on-board random generator
* <li>Creating text message transaction signatures on specialized readers
* <li>PIN unblocking using PUK codes
* </ul>
* <p>
* BeIDCard instances rely on an instance of BeIDCardUI to support user
* interaction, such as obtaining PIN and PUK codes for authentication, signing,
* verifying, changing PIN codes, and for notifying the user of the progress of
* such operations on a Secure Pinpad Device. A default implementation is
* available as DefaultBeIDCardUI, and unless replaced by an explicit call to
* setUI() will automatically be used (when present in the class path).
* <p>
* BeIDCard instances automatically detect CCID features in the underlying
* CardTerminal, and will choose the most secure path where several are
* available, for example, when needing to acquire PIN codes from the user, and
* the card is in a CCID-compliant Secure Pinpad Reader the PIN entry features
* of the reader will be used instead of the corresponding "obtain.." feature
* from the active BeIDCardUI. In that case, the corresponding "advise.." method
* of the active BeIDCardUI will be called instead, to advise the user to attend
* to the SPR.
* <p>
* To receive notifications of the progress of lengthy operations such as
* reading 'files' (certificates, photo,..) or signing (which may be lengthy
* because of user PIN interaction), register an instance of BeIDCardListener
* using addCardListener(). This is useful, for example, for providing progress
* indication to the user.
* <p>
* For detailed progress and error/debug logging, provide an instance of
* be.fedict.commons.eid.spi.Logger to BeIDCard's constructor (the default
* VoidLogger discards all logging and debug messages). You are advised to
* provide some form of logging facility, for all but the most trivial
* applications.
*
* @author Frank Cornelis
* @author Frank Marien
*
*/
public class BeIDCard {
private static final String UI_MISSING_LOG_MESSAGE = "No BeIDCardUI set and can't load DefaultBeIDCardUI";
private static final String UI_DEFAULT_REQUIRES_HEAD = "No BeIDCardUI set and DefaultBeIDCardUI requires a graphical environment";
private static final String DEFAULT_UI_IMPLEMENTATION = "be.fedict.commons.eid.dialogs.DefaultBeIDCardUI";
private static final byte[] BELPIC_AID = new byte[]{(byte) 0xA0, 0x00,
0x00, 0x01, 0x77, 0x50, 0x4B, 0x43, 0x53, 0x2D, 0x31, 0x35,};
private static final byte[] APPLET_AID = new byte[]{(byte) 0xA0, 0x00,
0x00, 0x00, 0x30, 0x29, 0x05, 0x70, 0x00, (byte) 0xAD, 0x13, 0x10,
0x01, 0x01, (byte) 0xFF,};
private static final int BLOCK_SIZE = 0xff;
private final CardChannel cardChannel;
private final List<BeIDCardListener> cardListeners;
private final CertificateFactory certificateFactory;
private final Card card;
private final Logger logger;
private CCID ccid;
private BeIDCardUI ui;
private CardTerminal cardTerminal;
private Locale locale;
/**
* Instantiate a BeIDCard from an already connected javax.smartcardio.Card,
* with a Logger implementation to receive logging output.
*
* @param card
* a javax.smartcardio.Card that you have previously determined
* to be a BeID Card
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger
* @throws IllegalArgumentException
* when passed a null logger. to disable logging, call
* BeIDCard(Card) instead.
* @throws RuntimeException
* when no CertificateFactory capable of producing X509
* Certificates is available.
*/
public BeIDCard(final Card card, final Logger logger) {
this.card = card;
this.cardChannel = card.getBasicChannel();
if (null == logger) {
throw new IllegalArgumentException("logger expected");
}
this.logger = logger;
this.cardListeners = new LinkedList<BeIDCardListener>();
try {
this.certificateFactory = CertificateFactory.getInstance("X.509");
} catch (final CertificateException e) {
throw new RuntimeException("X.509 algo", e);
}
}
/**
* Instantiate a BeIDCard from an already connected javax.smartcardio.Card
* no logging information will be available.
*
* @param card
* a javax.smartcardio.Card that you have previously determined
* to be a BeID Card
* @throws RuntimeException
* when no CertificateFactory capable of producing X509
* Certificates is available.
*/
public BeIDCard(final Card card) {
this(card, new VoidLogger());
}
/**
* Instantiate a BeIDCard from a javax.smartcardio.CardTerminal, with a
* Logger implementation to receive logging output.
*
* @param cardTerminal
* a javax.smartcardio.CardTerminal that you have previously
* determined to contain a BeID Card
* @param logger
* an instance of be.fedict.commons.eid.spi.Logger
* @throws IllegalArgumentException
* when passed a null logger. to disable logging, call public
* BeIDCard(CardTerminal) instead.
* @throws RuntimeException
* when no CertificateFactory capable of producing X509
* Certificates is available.
* @throws CardException
* in case of a smart card I/O error.
*/
public BeIDCard(final CardTerminal cardTerminal, final Logger logger)
throws CardException {
this(cardTerminal.connect("T=0"), logger);
}
/**
* Instantiate a BeIDCard from a javax.smartcardio.CardTerminal, with no
* logging information will be available.
*
* @param cardTerminal
* a javax.smartcardio.CardTerminal that you have previously
* determined to contain a BeID Card
* @throws RuntimeException
* when no CertificateFactory capable of producing X509
* Certificates is available.
* @throws CardException
* in case of a smart card I/O error.
*/
public BeIDCard(final CardTerminal cardTerminal) throws CardException {
this(cardTerminal.connect("T=0"), null);
setCardTerminal(cardTerminal);
}
/**
* close this BeIDCard, when you are done with it, to release any underlying
* resources. All subsequent calls will fail.
*
* @return this BeIDCard instance, to allow method chaining
*/
public BeIDCard close() {
this.logger.debug("closing eID card");
setCardTerminal(null);
try {
this.card.disconnect(true);
} catch (final CardException e) {
this.logger.error("could not disconnect the card: "
+ e.getMessage());
}
return this;
}
/**
* Explicitly set the User Interface to be used for consequent operations.
* All user interaction is handled through this, and possible SPR features
* of CCID-capable CardReaders. This will also modify the Locale setting of
* this beIDCard instance to match the UI's Locale, so the language in any
* SPR messages displayed will be consistent with the UI's language.
*
* @param userInterface
* an instance of BeIDCardUI
* @return this BeIDCard instance, to allow method chaining
*/
public final BeIDCard setUI(final BeIDCardUI userInterface) {
this.ui = userInterface;
if (this.locale == null) {
setLocale(userInterface.getLocale());
}
return this;
}
/**
* Register a BeIDCardListener to receive updates on any consequent file
* reading/signature operations executed by this BeIDCard.
*
* @param beIDCardListener
* a beIDCardListener instance
* @return this BeIDCard instance, to allow method chaining
*/
public final BeIDCard addCardListener(
final BeIDCardListener beIDCardListener) {
synchronized (this.cardListeners) {
this.cardListeners.add(beIDCardListener);
}
return this;
}
/**
* Unregister a BeIDCardListener to no longer receive updates on any
* consequent file reading/signature operations executed by this BeIDCard.
*
* @param beIDCardListener
* a beIDCardListener instance
* @return this BeIDCard instance, to allow method chaining
*/
public final BeIDCard removeCardListener(
final BeIDCardListener beIDCardListener) {
synchronized (this.cardListeners) {
this.cardListeners.remove(beIDCardListener);
}
return this;
}
/**
* Reads a certain certificate from the card. Which certificate to read is
* determined by the FileType param. Applicable FileTypes are
* AuthentificationCertificate, NonRepudiationCertificate, CACertificate,
* RootCertificate and RRNCertificate.
*
* @param fileType
* @return the certificate requested
* @throws CertificateException
* @throws CardException
* @throws IOException
* @throws InterruptedException
*/
public X509Certificate getCertificate(final FileType fileType)
throws CertificateException, CardException, IOException,
InterruptedException {
return (X509Certificate) this.certificateFactory
.generateCertificate(new ByteArrayInputStream(
readFile(fileType)));
}
/**
* Returns the X509 authentication certificate. This is a convenience method
* for <code>getCertificate(FileType.AuthentificationCertificate)</code>
*
* @return the X509 Authentication Certificate from the card.
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public X509Certificate getAuthenticationCertificate() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificate(FileType.AuthentificationCertificate);
}
/**
* Returns the X509 non-repudiation certificate. This is a convencience
* method for
* <code>getCertificate(FileType.NonRepudiationCertificate)</code>
*
* @return
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public X509Certificate getSigningCertificate() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificate(FileType.NonRepudiationCertificate);
}
/**
* Returns the citizen CA certificate. This is a convenience method for
* <code>getCertificate(FileType.CACertificate)</code>
*
* @return
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public X509Certificate getCACertificate() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificate(FileType.CACertificate);
}
/**
* Returns the Root CA certificate.
*
* @return the Root CA X509 certificate.
* @throws CertificateException
* @throws CardException
* @throws IOException
* @throws InterruptedException
*/
public X509Certificate getRootCACertificate() throws CertificateException,
CardException, IOException, InterruptedException {
return this.getCertificate(FileType.RootCertificate);
}
/**
* Returns the national registration certificate. This is a convencience
* method for <code>getCertificate(FileType.RRNCertificate)</code>
*
* @return
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public X509Certificate getRRNCertificate() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificate(FileType.RRNCertificate);
}
/**
* Returns the entire certificate chain for a given file type. Of course,
* only file types corresponding with a certificate are accepted. Which
* certificate's chain to return is determined by the FileType param.
* Applicable FileTypes are AuthentificationCertificate,
* NonRepudiationCertificate, CACertificate, and RRNCertificate.
*
* @param fileType
* which certificate's chain to return
* @return the certificate's chain up to and including the Belgian Root Cert
* @throws CertificateException
* @throws CardException
* @throws IOException
* @throws InterruptedException
*/
public List<X509Certificate> getCertificateChain(final FileType fileType)
throws CertificateException, CardException, IOException,
InterruptedException {
final List<X509Certificate> chain = new LinkedList<X509Certificate>();
chain.add((X509Certificate) this.certificateFactory
.generateCertificate(new ByteArrayInputStream(
readFile(fileType))));
if (fileType.chainIncludesCitizenCA()) {
chain.add((X509Certificate) this.certificateFactory
.generateCertificate(new ByteArrayInputStream(
readFile(FileType.CACertificate))));
}
chain.add((X509Certificate) this.certificateFactory
.generateCertificate(new ByteArrayInputStream(
readFile(FileType.RootCertificate))));
return chain;
}
/**
* Returns the X509 authentication certificate chain. (Authentication ->
* Citizen CA -> Root) This is a convenience method for
* <code>getCertificateChain(FileType.AuthentificationCertificate)</code>
*
* @return the authentication certificate chain
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public List<X509Certificate> getAuthenticationCertificateChain()
throws CardException, IOException, CertificateException,
InterruptedException {
return this.getCertificateChain(FileType.AuthentificationCertificate);
}
/**
* Returns the X509 non-repudiation certificate chain. (Non-Repudiation ->
* Citizen CA -> Root) This is a convenience method for
* <code>getCertificateChain(FileType.NonRepudiationCertificate)</code>
*
* @return the non-repudiation certificate chain
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public List<X509Certificate> getSigningCertificateChain()
throws CardException, IOException, CertificateException,
InterruptedException {
return this.getCertificateChain(FileType.NonRepudiationCertificate);
}
/**
* Returns the Citizen CA X509 certificate chain. (Citizen CA -> Root) This
* is a convenience method for
* <code>getCertificateChain(FileType.CACertificate)</code>
*
* @return the citizen ca certificate chain
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public List<X509Certificate> getCACertificateChain() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificateChain(FileType.CACertificate);
}
/**
* Returns the national registry X509 certificate chain. (National Registry
* -> Root) This is a convenience method for
* <code>getCertificateChain(FileType.RRNCertificate)</code>
*
* @return the national registry certificate chain
* @throws CardException
* @throws IOException
* @throws CertificateException
* @throws InterruptedException
*/
public List<X509Certificate> getRRNCertificateChain() throws CardException,
IOException, CertificateException, InterruptedException {
return this.getCertificateChain(FileType.RRNCertificate);
}
/**
* Sign a given digest value.
*
* @param digestValue
* the digest value to be signed.
* @param digestAlgo
* the algorithm used to calculate the given digest value.
* @param fileType
* the certificate's file type.
* @param requireSecureReader
* <code>true</code> if a secure pinpad reader is required.
* @return
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] sign(final byte[] digestValue, final BeIDDigest digestAlgo,
final FileType fileType, final boolean requireSecureReader)
throws CardException, IOException, InterruptedException,
UserCancelledException {
return sign(digestValue, digestAlgo, fileType, requireSecureReader,
null);
}
/**
* Sign a given digest value.
*
* @param digestValue
* the digest value to be signed.
* @param digestAlgo
* the algorithm used to calculate the given digest value.
* @param fileType
* the certificate's file type.
* @param requireSecureReader
* <code>true</code> if a secure pinpad reader is required.
* @param applicationName
* the optional application name.
* @return
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] sign(final byte[] digestValue, final BeIDDigest digestAlgo,
final FileType fileType, final boolean requireSecureReader,
final String applicationName) throws CardException, IOException,
InterruptedException, UserCancelledException {
if (!fileType.isCertificateUserCanSignWith()) {
throw new IllegalArgumentException(
"Not a certificate that can be used for signing: "
+ fileType.name());
}
if (getCCID().hasFeature(CCID.FEATURE.EID_PIN_PAD_READER)) {
this.logger.debug("eID-aware secure PIN pad reader detected");
}
if (requireSecureReader
&& (!getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_DIRECT))
&& (getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_START))) {
throw new SecurityException("not a secure reader");
}
this.beginExclusive();
notifySigningBegin(fileType);
try {
// select the key
this.logger.debug("selecting key...");
ResponseAPDU responseApdu = transmitCommand(
BeIDCommandAPDU.SELECT_ALGORITHM_AND_PRIVATE_KEY,
new byte[]{(byte) 0x04, // length
// of
// following
// data
(byte) 0x80, digestAlgo.getAlgorithmReference(), // algorithm
// reference
(byte) 0x84, fileType.getKeyId(),}); // private key
// reference
if (0x9000 != responseApdu.getSW()) {
throw new ResponseAPDUException(
"SET (select algorithm and private key) error",
responseApdu);
}
if (FileType.NonRepudiationCertificate.getKeyId() == fileType
.getKeyId()) {
this.logger
.debug("non-repudiation key detected, immediate PIN verify");
verifyPin(PINPurpose.NonRepudiationSignature, applicationName);
}
final ByteArrayOutputStream digestInfo = new ByteArrayOutputStream();
digestInfo.write(digestAlgo.getPrefix(digestValue.length));
digestInfo.write(digestValue);
this.logger.debug("computing digital signature...");
responseApdu = transmitCommand(
BeIDCommandAPDU.COMPUTE_DIGITAL_SIGNATURE,
digestInfo.toByteArray());
if (0x9000 == responseApdu.getSW()) {
/*
* OK, we could use the card PIN caching feature.
*
* Notice that the card PIN caching also works when first doing
* an authentication after a non-repudiation signature.
*/
return responseApdu.getData();
}
if (0x6982 != responseApdu.getSW()) {
this.logger.debug("SW: "
+ Integer.toHexString(responseApdu.getSW()));
throw new ResponseAPDUException(
"compute digital signature error", responseApdu);
}
/*
* 0x6982 = Security status not satisfied, so we do a PIN
* verification before retrying.
*/
this.logger.debug("PIN verification required...");
verifyPin(PINPurpose.fromFileType(fileType), applicationName);
this.logger
.debug("computing digital signature (attempt #2 after PIN verification)...");
responseApdu = transmitCommand(
BeIDCommandAPDU.COMPUTE_DIGITAL_SIGNATURE,
digestInfo.toByteArray());
if (0x9000 != responseApdu.getSW()) {
throw new ResponseAPDUException(
"compute digital signature error", responseApdu);
}
return responseApdu.getData();
} finally {
this.endExclusive();
notifySigningEnd(fileType);
}
}
/**
* Create an authentication signature.
*
* @param toBeSigned
* the data to be signed
* @param requireSecureReader
* whether to require a secure pinpad reader to obtain the
* citizen's PIN if false, the current BeIDCardUI will be used in
* the absence of a secure pinpad reader. If true, an exception
* will be thrown unless a SPR is available
* @return a SHA-1 digest of the input data signed by the citizen's
* authentication key
* @throws NoSuchAlgorithmException
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] signAuthn(final byte[] toBeSigned,
final boolean requireSecureReader) throws NoSuchAlgorithmException,
CardException, IOException, InterruptedException,
UserCancelledException {
return signAuthn(toBeSigned, requireSecureReader, null);
}
/**
* Create an authentication signature.
*
* @param toBeSigned
* the data to be signed
* @param requireSecureReader
* whether to require a secure pinpad reader to obtain the
* citizen's PIN if false, the current BeIDCardUI will be used in
* the absence of a secure pinpad reader. If true, an exception
* will be thrown unless a SPR is available
* @param applicationName
* the optional application name.
* @return a SHA-1 digest of the input data signed by the citizen's
* authentication key
* @throws NoSuchAlgorithmException
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] signAuthn(final byte[] toBeSigned,
final boolean requireSecureReader, final String applicationName)
throws NoSuchAlgorithmException, CardException, IOException,
InterruptedException, UserCancelledException {
final MessageDigest messageDigest = BeIDDigest.SHA_1
.getMessageDigestInstance();
final byte[] digest = messageDigest.digest(toBeSigned);
return this.sign(digest, BeIDDigest.SHA_1,
FileType.AuthentificationCertificate, requireSecureReader,
applicationName);
}
/**
* Verifying PIN Code (without other actions, for testing PIN), using the
* most secure method available. Note that this still has the side effect of
* loading a successfully tests PIN into the PIN cache, so that unless the
* card is removed, a subsequent authentication attempt will not request the
* PIN, but proceed with the PIN given here.
*
* @throws IOException
* @throws CardException
* @throws InterruptedException
* @throws UserCancelledException
*/
public void verifyPin() throws IOException, CardException,
InterruptedException, UserCancelledException {
verifyPin(null);
}
/**
* Verifying PIN Code (without other actions, for testing PIN), using the
* most secure method available. Note that this still has the side effect of
* loading a successfully tests PIN into the PIN cache, so that unless the
* card is removed, a subsequent authentication attempt will not request the
* PIN, but proceed with the PIN given here.
*
* @param applicationName
* the optional application name.
* @throws IOException
* @throws CardException
* @throws InterruptedException
* @throws UserCancelledException
*/
public void verifyPin(String applicationName) throws IOException,
CardException, InterruptedException, UserCancelledException {
this.verifyPin(PINPurpose.PINTest, applicationName);
}
/**
* Change PIN code. This method will attempt to change PIN using the most
* secure method available. if requiresSecureReader is true, this will throw
* a SecurityException if no SPR is available, otherwise, this will default
* to changing the PIN via the UI
*
* @param requireSecureReader
* @throws Exception
*/
public void changePin(final boolean requireSecureReader) throws Exception {
if (requireSecureReader
&& (!getCCID().hasFeature(CCID.FEATURE.MODIFY_PIN_DIRECT))
&& (!getCCID().hasFeature(CCID.FEATURE.MODIFY_PIN_START))) {
throw new SecurityException("not a secure reader");
}
int retriesLeft = -1;
ResponseAPDU responseApdu;
do {
if (getCCID().hasFeature(CCID.FEATURE.MODIFY_PIN_START)) {
this.logger.debug("using modify pin start/finish...");
responseApdu = changePINViaCCIDStartFinish(retriesLeft);
} else if (getCCID().hasFeature(CCID.FEATURE.MODIFY_PIN_DIRECT)) {
this.logger.debug("could use direct PIN modify here...");
responseApdu = changePINViaCCIDDirect(retriesLeft);
} else {
responseApdu = changePINViaUI(retriesLeft);
}
if (0x9000 != responseApdu.getSW()) {
this.logger.debug("CHANGE PIN error");
this.logger.debug("SW: "
+ Integer.toHexString(responseApdu.getSW()));
if (0x6983 == responseApdu.getSW()) {
getUI().advisePINBlocked();
throw new ResponseAPDUException("eID card blocked!",
responseApdu);
}
if (0x63 != responseApdu.getSW1()) {
this.logger.debug("PIN change error. Card blocked?");
throw new ResponseAPDUException("PIN Change Error",
responseApdu);
}
retriesLeft = responseApdu.getSW2() & 0xf;
this.logger.debug("retries left: " + retriesLeft);
}
} while (0x9000 != responseApdu.getSW());
getUI().advisePINChanged();
}
/**
* Returns random data generated by the eID card itself.
*
* @param size
* the size of the requested random data.
* @return size bytes of random data
* @throws CardException
*/
public byte[] getChallenge(final int size) throws CardException {
final ResponseAPDU responseApdu = transmitCommand(
BeIDCommandAPDU.GET_CHALLENGE, new byte[]{}, 0, 0, size);
if (0x9000 != responseApdu.getSW()) {
this.logger.debug("get challenge failure: "
+ Integer.toHexString(responseApdu.getSW()));
throw new ResponseAPDUException("get challenge failure: "
+ Integer.toHexString(responseApdu.getSW()), responseApdu);
}
if (size != responseApdu.getData().length) {
throw new RuntimeException("challenge size incorrect: "
+ responseApdu.getData().length);
}
return responseApdu.getData();
}
/**
* Create a text message transaction signature. The FedICT eID aware secure
* pinpad readers can visualize such type of text message transactions on
* their hardware display.
*
* @param transactionMessage
* the transaction message to be signed.
* @param requireSecureReader
* @return
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] signTransactionMessage(final String transactionMessage,
final boolean requireSecureReader) throws CardException,
IOException, InterruptedException, UserCancelledException {
return signTransactionMessage(transactionMessage, requireSecureReader,
null);
}
/**
* Create a text message transaction signature. The FedICT eID aware secure
* pinpad readers can visualize such type of text message transactions on
* their hardware display.
*
* @param transactionMessage
* the transaction message to be signed.
* @param requireSecureReader
* @param applicationName
* the optional application name.
* @return
* @throws CardException
* @throws IOException
* @throws InterruptedException
* @throws UserCancelledException
*/
public byte[] signTransactionMessage(final String transactionMessage,
final boolean requireSecureReader, final String applicationName)
throws CardException, IOException, InterruptedException,
UserCancelledException {
if (getCCID().hasFeature(CCID.FEATURE.EID_PIN_PAD_READER)) {
getUI().adviseSecureReaderOperation();
}
byte[] signature;
try {
signature = this.sign(transactionMessage.getBytes(),
BeIDDigest.PLAIN_TEXT,
FileType.AuthentificationCertificate, requireSecureReader,
applicationName);
} finally {
if (getCCID().hasFeature(CCID.FEATURE.EID_PIN_PAD_READER)) {
getUI().adviseSecureReaderOperationEnd();
}
}
return signature;
}
/**
* Discard the citizen's PIN code from the PIN cache. Any subsequent
* Authentication signatures will require PIN entry. (non-repudation
* signatures are automatically protected)
*
* @throws Exception
* @return this BeIDCard instance, to allow method chaining
*/
public BeIDCard logoff() throws Exception {
final CommandAPDU logoffApdu = new CommandAPDU(0x80, 0xE6, 0x00, 0x00);
this.logger.debug("logoff...");
final ResponseAPDU responseApdu = transmit(logoffApdu);
if (0x9000 != responseApdu.getSW()) {
throw new RuntimeException("logoff failed");
}
return this;
}
/**
* Unblocking PIN using PUKs. This will choose the most secure method
* available to unblock a blocked PIN. If requireSecureReader is true, will
* throw SecurityException if an SPR is not available
*
* @param requireSecureReader
* @throws Exception
*/
public void unblockPin(final boolean requireSecureReader) throws Exception {
if (requireSecureReader
&& (!getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_DIRECT))) {
throw new SecurityException("not a secure reader");
}
ResponseAPDU responseApdu;
int retriesLeft = -1;
do {
if (getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_DIRECT)) {
this.logger.debug("could use direct PIN verify here...");
responseApdu = unblockPINViaCCIDVerifyPINDirectOfPUK(retriesLeft);
} else {
responseApdu = unblockPINViaUI(retriesLeft);
}
if (0x9000 != responseApdu.getSW()) {
this.logger.debug("PIN unblock error");
this.logger.debug("SW: "
+ Integer.toHexString(responseApdu.getSW()));
if (0x6983 == responseApdu.getSW()) {
getUI().advisePINBlocked();
throw new ResponseAPDUException("eID card blocked!",
responseApdu);
}
if (0x63 != responseApdu.getSW1()) {
this.logger.debug("PIN unblock error.");
throw new ResponseAPDUException("PIN unblock error",
responseApdu);
}
retriesLeft = responseApdu.getSW2() & 0xf;
this.logger.debug("retries left: " + retriesLeft);
}
} while (0x9000 != responseApdu.getSW());
getUI().advisePINUnblocked();
}
/**
* getATR returns the ATR of the eID Card. If this BeIDCard instance was
* constructed using the CardReader constructor, this is the only way to get
* to the ATR.
*
* @return
*/
public ATR getATR() {
return this.card.getATR();
}
/**
* @return the current Locale used in CCID SPR operations and UI
*/
public Locale getLocale() {
if (this.locale != null) {
return this.locale;
}
return LocaleManager.getLocale();
}
/**
* set the Locale to use for subsequent UI and CCID operations. this will
* modify the Locale of any explicitly set UI, as well. BeIDCard instances,
* while using the global Locale settings made in BeIDCards and/or
* BeIDCardManager by default, may have their own individual Locale settings
* that may override those global settings.
*
* @param newLocale
* @return this BeIDCard instance, to allow method chaining
*/
public BeIDCard setLocale(Locale newLocale) {
this.locale = newLocale;
if (this.locale != null && this.ui != null) {
this.ui.setLocale(this.locale);
}
return this;
}
// ===========================================================================================================
// low-level card operations
// not recommended for general use.
// if you find yourself having to call these, we'd very much like to hear
// about it.
// ===========================================================================================================
/**
* Select the BELPIC applet on the chip. Since the BELPIC applet is supposed
* to be all alone on the chip, shouldn't be necessary.
*
* @return this BeIDCard instance, to allow method chaining
* @throws CardException
*/
public BeIDCard selectApplet() throws CardException {
ResponseAPDU responseApdu;
responseApdu = transmitCommand(BeIDCommandAPDU.SELECT_APPLET_0,
BELPIC_AID);
if (0x9000 != responseApdu.getSW()) {
this.logger.error("error selecting BELPIC");
this.logger.debug("status word: "
+ Integer.toHexString(responseApdu.getSW()));
/*
* Try to select the Applet.
*/
try {
responseApdu = transmitCommand(BeIDCommandAPDU.SELECT_APPLET_1,
APPLET_AID);
} catch (final CardException e) {
this.logger.error("error selecting Applet");
return this;
}
if (0x9000 != responseApdu.getSW()) {
this.logger.error("could not select applet");
} else {
this.logger
.debug("BELPIC JavaCard applet selected by APPLET_AID");
}
} else {
this.logger.debug("BELPIC JavaCard applet selected by BELPIC_AID");
}
return this;
}
// --------------------------------------------------------------------------------------------------------------------------------
/**
* Begin an exclusive transaction with the card. Once this returns, only the
* calling thread will be able to access the card, until it calls
* endExclusive(). Other threads will receive a CardException. Use this when
* you need to make several calls to the card that depend on each other. for
* example, SELECT FILE and READ BINARY, or SELECT ALGORITHM and COMPUTE
* SIGNATURE, to avoid other threads/processes from interleaving commands
* that would break your transactional logic.
*
* Called automatically by the higher-level methods in this class. If you
* end up calling this directly, this is either something wrong with your
* code, or with this class. Please let us know. You should really only have
* to be calling this when using some of the other low-level methods
* (transmitCommand, etc..) *never* in combination with the high-level
* methods.
*
* @return this BeIDCard Instance, to allow method chaining.
* @throws CardException
*/
public BeIDCard beginExclusive() throws CardException {
this.logger.debug("---begin exclusive---");
this.card.beginExclusive();
return this;
}
/**
* Release an exclusive transaction with the card, started by
* beginExclusive().
*
* @return this BeIDCard Instance, to allow method chaining.
* @throws CardException
*/
public BeIDCard endExclusive() throws CardException {
this.logger.debug("---end exclusive---");
try {
this.card.endExclusive();
} catch (CardException e) {
this.logger.error("end exclusive failed: " + e.getMessage());
}
return this;
}
// --------------------------------------------------------------------------------------------------------------------------------
/**
* Read bytes from a previously selected "File" on the card. should be
* preceded by a call to selectFile so the card knows what you want to read.
* Consider using one of the higher-level methods, or readFile().
*
* @param fileType
* the file to read (to allow for notification)
* @param estimatedMaxSize
* the estimated total size of the file to read (to allow for
* notification)
* @return the data from the file
* @throws CardException
* @throws IOException
* @throws InterruptedException
*/
public byte[] readBinary(final FileType fileType, final int estimatedMaxSize)
throws CardException, IOException, InterruptedException {
int offset = 0;
this.logger.debug("read binary");
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data;
do {
if (Thread.currentThread().isInterrupted()) {
this.logger.debug("interrupted in readBinary");
throw new InterruptedException();
}
notifyReadProgress(fileType, offset, estimatedMaxSize);
final ResponseAPDU responseApdu = transmitCommand(
BeIDCommandAPDU.READ_BINARY, offset >> 8, offset & 0xFF,
BLOCK_SIZE);
final 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) {
final IOException ioEx = new IOException(
"BeIDCommandAPDU response error: "
+ responseApdu.getSW(),
new ResponseAPDUException(responseApdu));
throw ioEx;
}
data = responseApdu.getData();
baos.write(data);
offset += data.length;
} while (BLOCK_SIZE == data.length);
notifyReadProgress(fileType, offset, offset);
return baos.toByteArray();
}
/**
* Selects a file to read on the card
*
* @param fileId
* the file to read
* @return this BeIDCard Instance, to allow method chaining.
* @throws CardException
* @throws FileNotFoundException
*/
public BeIDCard selectFile(final byte[] fileId) throws CardException,
FileNotFoundException {
this.logger.debug("selecting file");
final ResponseAPDU responseApdu = transmitCommand(
BeIDCommandAPDU.SELECT_FILE, fileId);
if (0x9000 != responseApdu.getSW()) {
final FileNotFoundException fnfEx = new FileNotFoundException(
"wrong status word after selecting file: "
+ Integer.toHexString(responseApdu.getSW()));
fnfEx.initCause(new ResponseAPDUException(responseApdu));
throw fnfEx;
}
try {
// SCARD_E_SHARING_VIOLATION fix
Thread.sleep(20);
} catch (final InterruptedException e) {
throw new RuntimeException("sleep error: " + e.getMessage());
}
return this;
}
/**
* Reads a file from the card.
*
* @param fileType
* the file to read
* @return the data from the file
* @throws CardException
* @throws IOException
* @throws InterruptedException
*/
public byte[] readFile(final FileType fileType) throws CardException,
IOException, InterruptedException {
this.beginExclusive();
try {
this.selectFile(fileType.getFileId());
return this.readBinary(fileType, fileType.getEstimatedMaxSize());
} finally {
this.endExclusive();
}
}
/**
* test for CCID Features in the card reader this BeIDCard is inserted into
*
* @param feature
* the feature to test for (CCID.FEATURE)
* @return true if the given feature is available, false if not
*/
public boolean cardTerminalHasCCIDFeature(CCID.FEATURE feature) {
return this.getCCID().hasFeature(feature);
}
public byte[] getCardData() throws CardException, FileNotFoundException {
ResponseAPDU responseApdu = transmitCommand(
BeIDCommandAPDU.GET_CARD_DATA, 0xff);
if (0x9000 != responseApdu.getSW()) {
throw new FileNotFoundException("GET CARD DATA ERROR: "
+ Integer.toHexString(responseApdu.getSW()));
}
return responseApdu.getData();
}
// ===========================================================================================================
// low-level card transmit commands
// not recommended for general use.
// if you find yourself having to call these, we'd very much like to hear
// about it.
// ===========================================================================================================
protected byte[] transmitCCIDControl(final boolean usePPDU,
final CCID.FEATURE feature) throws CardException {
return transmitControlCommand(getCCID().getFeature(feature),
new byte[0]);
}
protected byte[] transmitCCIDControl(final boolean usePPDU,
final CCID.FEATURE feature, final byte[] command)
throws CardException {
if (usePPDU) {
return transmitPPDUCommand(feature.getTag(), command);
} else {
return transmitControlCommand(getCCID().getFeature(feature),
command);
}
}
protected byte[] transmitControlCommand(final int controlCode,
final byte[] command) throws CardException {
return this.card.transmitControlCommand(controlCode, command);
}
protected byte[] transmitPPDUCommand(final int controlCode,
final byte[] command) throws CardException {
ResponseAPDU responseAPDU = transmitCommand(BeIDCommandAPDU.PPDU,
controlCode, command);
if (responseAPDU.getSW() != 0x9000) {
throw new CardException("PPDU Command Failed: ResponseAPDU="
+ responseAPDU.getSW());
}
if (responseAPDU.getNr() == 0) {
return responseAPDU.getBytes();
}
return responseAPDU.getData();
}
protected ResponseAPDU transmitCommand(final BeIDCommandAPDU apdu,
final int le) throws CardException {
return transmit(new CommandAPDU(apdu.getCla(), apdu.getIns(),
apdu.getP1(), apdu.getP2(), le));
}
protected ResponseAPDU transmitCommand(final BeIDCommandAPDU apdu,
final int p2, final byte[] data) throws CardException {
return transmit(new CommandAPDU(apdu.getCla(), apdu.getIns(),
apdu.getP1(), p2, data));
}
protected ResponseAPDU transmitCommand(final BeIDCommandAPDU apdu,
final int p1, final int p2, final int le) throws CardException {
return transmit(new CommandAPDU(apdu.getCla(), apdu.getIns(), p1, p2,
le));
}
protected ResponseAPDU transmitCommand(final BeIDCommandAPDU apdu,
final byte[] data) throws CardException {
return transmit(new CommandAPDU(apdu.getCla(), apdu.getIns(),
apdu.getP1(), apdu.getP2(), data));
}
protected ResponseAPDU transmitCommand(final BeIDCommandAPDU apdu,
final byte[] data, final int dataOffset, final int dataLength,
final int ne) throws CardException {
return transmit(new CommandAPDU(apdu.getCla(), apdu.getIns(),
apdu.getP1(), apdu.getP2(), data, dataOffset, dataLength, ne));
}
private ResponseAPDU transmit(final CommandAPDU commandApdu)
throws CardException {
return transmit(commandApdu, 0);
}
private ResponseAPDU transmit(final CommandAPDU commandApdu,
final int attempt) throws CardException {
if (attempt >= 32) {
throw new CardException("Could not obtain response.");
}
ResponseAPDU responseApdu = this.cardChannel.transmit(commandApdu);
if (0x6c == responseApdu.getSW1()) {
/*
* A minimum delay of 10 msec between the answer ?????????6C
* xx????????? and the next BeIDCommandAPDU is mandatory for eID
* v1.0 and v1.1 cards.
*/
this.logger.debug("sleeping...");
try {
Thread.sleep(10);
} catch (final InterruptedException e) {
throw new RuntimeException("cannot sleep");
}
CommandAPDU newCommandApdu = new CommandAPDU(commandApdu.getCLA(),
commandApdu.getINS(), commandApdu.getP1(),
commandApdu.getP2(), commandApdu.getData(),
responseApdu.getSW2());
responseApdu = transmit(newCommandApdu, attempt + 1);
} else if (0x61 == responseApdu.getSW1()) {
/*
* Issue a GET RESPONSE command to retrieve the remaining data.
*/
int le = responseApdu.getSW2();
if (le == 0) {
le = 0xff;
}
CommandAPDU newCommandApdu = new CommandAPDU(0x00, 0xC0, 0x00,
0x00, le);
ResponseAPDU newResponseApdu = transmit(newCommandApdu, attempt + 1);
/*
* Combine the previously received data with the new response.
*/
byte[] oldData = responseApdu.getData();
byte[] newResponse = newResponseApdu.getBytes();
byte[] combined = new byte[oldData.length + newResponse.length];
System.arraycopy(oldData, 0, combined, 0, oldData.length);
System.arraycopy(newResponse, 0, combined, oldData.length,
newResponse.length);
responseApdu = new ResponseAPDU(combined);
}
return responseApdu;
}
// ===========================================================================================================
// notifications of listeners
// ===========================================================================================================
private void notifyReadProgress(final FileType fileType, final int offset,
int estimatedMaxOffset) {
if (offset > estimatedMaxOffset) {
estimatedMaxOffset = offset;
}
synchronized (this.cardListeners) {
for (BeIDCardListener listener : this.cardListeners) {
try {
listener.notifyReadProgress(fileType, offset,
estimatedMaxOffset);
} catch (final Exception ex) {
this.logger
.debug("Exception Thrown In BeIDCardListener.notifyReadProgress():"
+ ex.getMessage());
}
}
}
}
private void notifySigningBegin(final FileType keyType) {
synchronized (this.cardListeners) {
for (BeIDCardListener listener : this.cardListeners) {
try {
listener.notifySigningBegin(keyType);
} catch (final Exception ex) {
this.logger
.debug("Exception Thrown In BeIDCardListener.notifySigningBegin():"
+ ex.getMessage());
}
}
}
}
private void notifySigningEnd(final FileType keyType) {
synchronized (this.cardListeners) {
for (BeIDCardListener listener : this.cardListeners) {
try {
listener.notifySigningEnd(keyType);
} catch (final Exception ex) {
this.logger
.debug("Exception Thrown In BeIDCardListener.notifySigningBegin():"
+ ex.getMessage());
}
}
}
}
// ===========================================================================================================
// various PIN-related implementations
// ===========================================================================================================
/*
* Verify PIN code for purpose "purpose" This method will attempt to verify
* PIN using the most secure method available. If that method turns out to
* be the UI, will pass purpose to the UI.
*/
private void verifyPin(final PINPurpose purpose,
final String applicationName) throws IOException, CardException,
InterruptedException, UserCancelledException {
ResponseAPDU responseApdu;
int retriesLeft = -1;
do {
if (getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_DIRECT)) {
responseApdu = verifyPINViaCCIDDirect(retriesLeft, purpose,
applicationName);
} else if (getCCID().hasFeature(CCID.FEATURE.VERIFY_PIN_START)) {
responseApdu = verifyPINViaCCIDStartFinish(retriesLeft,
purpose, applicationName);
} else {
responseApdu = verifyPINViaUI(retriesLeft, purpose,
applicationName);
}
if (0x9000 != responseApdu.getSW()) {
this.logger.debug("VERIFY_PIN error");
this.logger.debug("SW: "
+ Integer.toHexString(responseApdu.getSW()));
if (0x6983 == responseApdu.getSW()) {
getUI().advisePINBlocked();
throw new ResponseAPDUException("eID card blocked!",
responseApdu);
}
if (0x63 != responseApdu.getSW1()) {
this.logger.debug("PIN verification error.");
throw new ResponseAPDUException("PIN Verification Error",
responseApdu);
}
retriesLeft = responseApdu.getSW2() & 0xf;
this.logger.debug("retries left: " + retriesLeft);
}
} while (0x9000 != responseApdu.getSW());
}
/*
* Verify PIN code using CCID Direct PIN Verify sequence.
*/
private ResponseAPDU verifyPINViaCCIDDirect(final int retriesLeft,
PINPurpose purpose, String applicationName) throws IOException,
CardException {
this.logger.debug("direct PIN verification...");
getUI().advisePINPadPINEntry(retriesLeft, purpose, applicationName);
byte[] result;
try {
result = this.transmitCCIDControl(
getCCID().usesPPDU(),
CCID.FEATURE.VERIFY_PIN_DIRECT,
getCCID().createPINVerificationDataStructure(
this.getLocale(), CCID.INS.VERIFY_PIN));
} finally {
getUI().advisePINPadOperationEnd();
}
final ResponseAPDU responseApdu = new ResponseAPDU(result);
if (0x6401 == responseApdu.getSW()) {
this.logger.debug("canceled by user");
final SecurityException securityException = new SecurityException(
"canceled by user", new ResponseAPDUException(responseApdu));
throw securityException;
} else if (0x6400 == responseApdu.getSW()) {
this.logger.debug("PIN pad timeout");
}
return responseApdu;
}
/*
* Verify PIN code using CCID Start/Finish sequence.
*/
private ResponseAPDU verifyPINViaCCIDStartFinish(final int retriesLeft,
PINPurpose purpose, String applicationName) throws IOException,
CardException, InterruptedException {
this.logger.debug("CCID verify PIN start/end sequence...");
getUI().advisePINPadPINEntry(retriesLeft, purpose, applicationName);
try {
this.transmitCCIDControl(
getCCID().usesPPDU(),
CCID.FEATURE.VERIFY_PIN_START,
getCCID().createPINVerificationDataStructure(
this.getLocale(), CCID.INS.VERIFY_PIN));
getCCID().waitForOK();
} finally {
getUI().advisePINPadOperationEnd();
}
return new ResponseAPDU(this.transmitCCIDControl(getCCID().usesPPDU(),
CCID.FEATURE.VERIFY_PIN_FINISH));
}
private boolean isWindows8() {
final String osName = System.getProperty("os.name");
boolean win8 = osName.contains("Windows 8");
if (win8) {
return true;
}
boolean win10 = osName.contains("Windows 10");
if (win10) {
return true;
}
return false;
}
/*
* Verify PIN code by obtaining it from the current UI
*/
private ResponseAPDU verifyPINViaUI(final int retriesLeft,
final PINPurpose purpose, final String applicationName)
throws CardException, UserCancelledException {
final boolean windows8 = this.isWindows8();
if (windows8) {
this.endExclusive();
}
final char[] pin = getUI().obtainPIN(retriesLeft, purpose,
applicationName);
if (windows8) {
this.beginExclusive();
}
final byte[] verifyData = new byte[]{(byte) (0x20 | pin.length),
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF,};
for (int idx = 0; idx < pin.length; idx += 2) {
final char digit1 = pin[idx];
final char digit2;
if (idx + 1 < pin.length) {
digit2 = pin[idx + 1];
} else {
digit2 = '0' + 0xf;
}
final byte value = (byte) (byte) ((digit1 - '0' << 4) + (digit2 - '0'));
verifyData[idx / 2 + 1] = value;
}
Arrays.fill(pin, (char) 0); // minimize exposure
this.logger.debug("verifying PIN...");
try {
return this.transmitCommand(BeIDCommandAPDU.VERIFY_PIN, verifyData);
} finally {
Arrays.fill(verifyData, (byte) 0); // minimize exposure
}
}
/*
* Modify PIN code using CCID Direct PIN Modify sequence.
*/
private ResponseAPDU changePINViaCCIDDirect(final int retriesLeft)
throws IOException, CardException {
this.logger.debug("direct PIN modification...");
getUI().advisePINPadChangePIN(retriesLeft);
byte[] result;
try {
result = this.transmitCCIDControl(
getCCID().usesPPDU(),
CCID.FEATURE.MODIFY_PIN_DIRECT,
this.getCCID().createPINModificationDataStructure(
this.getLocale(), CCID.INS.MODIFY_PIN));
} finally {
getUI().advisePINPadOperationEnd();
}
final ResponseAPDU responseApdu = new ResponseAPDU(result);
if (0x6402 == responseApdu.getSW()) {
this.logger.debug("PINs differ");
} else if (0x6401 == responseApdu.getSW()) {
this.logger.debug("canceled by user");
final SecurityException securityException = new SecurityException(
"canceled by user", new ResponseAPDUException(responseApdu));
throw securityException;
} else if (0x6400 == responseApdu.getSW()) {
this.logger.debug("PIN pad timeout");
}
return responseApdu;
}
/*
* Modify PIN code using CCID Modify PIN Start sequence
*/
private ResponseAPDU changePINViaCCIDStartFinish(final int retriesLeft)
throws IOException, CardException, InterruptedException {
this.transmitCCIDControl(
getCCID().usesPPDU(),
CCID.FEATURE.MODIFY_PIN_START,
getCCID().createPINModificationDataStructure(this.getLocale(),
CCID.INS.MODIFY_PIN));
try {
this.logger.debug("enter old PIN...");
getUI().advisePINPadOldPINEntry(retriesLeft);
getCCID().waitForOK();
getUI().advisePINPadOperationEnd();
this.logger.debug("enter new PIN...");
getUI().advisePINPadNewPINEntry(retriesLeft);
getCCID().waitForOK();
getUI().advisePINPadOperationEnd();
this.logger.debug("enter new PIN again...");
getUI().advisePINPadNewPINEntryAgain(retriesLeft);
getCCID().waitForOK();
} finally {
getUI().advisePINPadOperationEnd();
}
return new ResponseAPDU(this.transmitCCIDControl(getCCID().usesPPDU(),
CCID.FEATURE.MODIFY_PIN_FINISH));
}
/*
* Modify PIN via the UI
*/
private ResponseAPDU changePINViaUI(final int retriesLeft)
throws CardException {
final char[][] pins = getUI().obtainOldAndNewPIN(retriesLeft);
final char[] oldPin = pins[0];
final char[] newPin = pins[1];
final byte[] changePinData = new byte[]{(byte) (0x20 | oldPin.length),
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) (0x20 | newPin.length), (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF,};
for (int idx = 0; idx < oldPin.length; idx += 2) {
final char digit1 = oldPin[idx];
final char digit2;
if (idx + 1 < oldPin.length) {
digit2 = oldPin[idx + 1];
} else {
digit2 = '0' + 0xf;
}
final byte value = (byte) (byte) ((digit1 - '0' << 4) + (digit2 - '0'));
changePinData[idx / 2 + 1] = value;
}
Arrays.fill(oldPin, (char) 0); // minimize exposure
for (int idx = 0; idx < newPin.length; idx += 2) {
final char digit1 = newPin[idx];
final char digit2;
if (idx + 1 < newPin.length) {
digit2 = newPin[idx + 1];
} else {
digit2 = '0' + 0xf;
}
final byte value = (byte) (byte) ((digit1 - '0' << 4) + (digit2 - '0'));
changePinData[(idx / 2 + 1) + 8] = value;
}
Arrays.fill(newPin, (char) 0); // minimize exposure
try {
return this.transmitCommand(BeIDCommandAPDU.CHANGE_PIN,
changePinData);
} finally {
Arrays.fill(changePinData, (byte) 0);
}
}
/*
* Unblock PIN using CCID Verify PIN Direct sequence on the PUK
*/
private ResponseAPDU unblockPINViaCCIDVerifyPINDirectOfPUK(
final int retriesLeft) throws IOException, CardException {
this.logger.debug("direct PUK verification...");
getUI().advisePINPadPUKEntry(retriesLeft);
byte[] result;
try {
result = this.transmitCCIDControl(
getCCID().usesPPDU(),
CCID.FEATURE.VERIFY_PIN_DIRECT,
this.getCCID().createPINVerificationDataStructure(
this.getLocale(), CCID.INS.VERIFY_PUK));
} finally {
getUI().advisePINPadOperationEnd();
}
final ResponseAPDU responseApdu = new ResponseAPDU(result);
if (0x6401 == responseApdu.getSW()) {
this.logger.debug("canceled by user");
final SecurityException securityException = new SecurityException(
"canceled by user", new ResponseAPDUException(responseApdu));
throw securityException;
} else if (0x6400 == responseApdu.getSW()) {
this.logger.debug("PIN pad timeout");
}
return responseApdu;
}
/*
* Unblock the PIN by obtaining PUK codes from the UI and calling RESET_PIN
* on the card.
*/
private ResponseAPDU unblockPINViaUI(final int retriesLeft)
throws CardException {
final char[][] puks = getUI().obtainPUKCodes(retriesLeft);
final char[] puk1 = puks[0];
final char[] puk2 = puks[1];
final char[] fullPuk = new char[puk1.length + puk2.length];
System.arraycopy(puk2, 0, fullPuk, 0, puk2.length);
Arrays.fill(puk2, (char) 0);
System.arraycopy(puk1, 0, fullPuk, puk2.length, puk1.length);
Arrays.fill(puk1, (char) 0);
final byte[] unblockPinData = new byte[]{
(byte) (0x20 | ((byte) (puk1.length + puk2.length))),
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF,};
for (int idx = 0; idx < fullPuk.length; idx += 2) {
final char digit1 = fullPuk[idx];
final char digit2 = fullPuk[idx + 1];
final byte value = (byte) (byte) ((digit1 - '0' << 4) + (digit2 - '0'));
unblockPinData[idx / 2 + 1] = value;
}
Arrays.fill(fullPuk, (char) 0); // minimize exposure
try {
return this.transmitCommand(BeIDCommandAPDU.RESET_PIN,
unblockPinData);
} finally {
Arrays.fill(unblockPinData, (byte) 0);
}
}
// ----------------------------------------------------------------------------------------------------------------------------------
private CCID getCCID() {
if (this.ccid == null) {
this.ccid = new CCID(this.card, this.cardTerminal, this.logger);
}
return this.ccid;
}
private BeIDCardUI getUI() {
if (this.ui == null) {
if (GraphicsEnvironment.isHeadless()) {
this.logger.error(UI_DEFAULT_REQUIRES_HEAD);
throw new UnsupportedOperationException(
UI_DEFAULT_REQUIRES_HEAD);
}
try {
final ClassLoader classLoader = BeIDCard.class.getClassLoader();
final Class<?> uiClass = classLoader
.loadClass(DEFAULT_UI_IMPLEMENTATION);
this.ui = (BeIDCardUI) uiClass.newInstance();
if (this.locale != null) {
this.ui.setLocale(this.locale);
}
} catch (final Exception e) {
this.logger.error(UI_MISSING_LOG_MESSAGE);
throw new UnsupportedOperationException(UI_MISSING_LOG_MESSAGE,
e);
}
}
return this.ui;
}
/**
* Return the CardTerminal that held this BeIdCard when it was detected Will
* return null if the physical Card that we represent was removed.
*
* @return the cardTerminal this BeIDCard was in when detected, or null
*/
public CardTerminal getCardTerminal() {
return this.cardTerminal;
}
/**
*
* @param cardTerminal
*/
public void setCardTerminal(CardTerminal cardTerminal) {
this.cardTerminal = cardTerminal;
}
/*
* BeIDCommandAPDU encapsulates values sent in CommandAPDU's, to make these
* more readable in BeIDCard.
*/
private enum BeIDCommandAPDU {
SELECT_APPLET_0(0x00, 0xA4, 0x04, 0x0C), // TODO these are the same?
SELECT_APPLET_1(0x00, 0xA4, 0x04, 0x0C), // TODO see above
SELECT_FILE(0x00, 0xA4, 0x08, 0x0C),
READ_BINARY(0x00, 0xB0),
VERIFY_PIN(0x00, 0x20, 0x00, 0x01),
CHANGE_PIN(0x00, 0x24, 0x00, 0x01), // 0x0024=change
// reference
// change
SELECT_ALGORITHM_AND_PRIVATE_KEY(0x00, 0x22, 0x41, 0xB6), // ISO 7816-8
// SET
// COMMAND
// (select
// algorithm and
// key for
// signature)
COMPUTE_DIGITAL_SIGNATURE(0x00, 0x2A, 0x9E, 0x9A), // ISO 7816-8 COMPUTE
// DIGITAL SIGNATURE
// COMMAND
RESET_PIN(0x00, 0x2C, 0x00, 0x01),
GET_CHALLENGE(0x00, 0x84, 0x00, 0x00),
GET_CARD_DATA(0x80, 0xE4, 0x00, 0x00),
PPDU(0xFF, 0xC2, 0x01);
private final int cla;
private final int ins;
private final int p1;
private final int p2;
private BeIDCommandAPDU(final int cla, final int ins, final int p1,
final int p2) {
this.cla = cla;
this.ins = ins;
this.p1 = p1;
this.p2 = p2;
}
private BeIDCommandAPDU(final int cla, final int ins, final int p1) {
this.cla = cla;
this.ins = ins;
this.p1 = p1;
this.p2 = -1;
}
private BeIDCommandAPDU(final int cla, final int ins) {
this.cla = cla;
this.ins = ins;
this.p1 = -1;
this.p2 = -1;
}
public int getCla() {
return this.cla;
}
public int getIns() {
return this.ins;
}
public int getP1() {
return this.p1;
}
public int getP2() {
return this.p2;
}
}
}