/*
 * @(#)JavaCardCCProxyService.java        1.0 09/06/1999
 * 
 * Copyright (c) 1994-1999 Sun Microsystems, Inc.
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 * 
 */

/*
 * JavaCardCCProxyService.java --
 * 
 * Copyright (c) 1998-1999 Sun Microsystem, Inc.
 * All rights reserved, all wrongs reversed.
 * 
 * SCCS: @(#) JavaCardCCProxyService.java 1.22 99/03/26 01:40:51
 */
package sunlabs.oc.javacard.proxies;

import sunlabs.sts.ByteArray;
import sunlabs.oc.ChainHashtable;
import sunlabs.oc.ISO7816Status;
import sunlabs.oc.purse.PurseCardService;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import opencard.core.OpenCardException;
import opencard.core.service.CardChannel;
import opencard.core.service.CardService;
import opencard.core.service.CardServiceException;
import opencard.core.service.CardServiceInabilityException;
import opencard.core.service.CardServiceOperationFailedException;
import opencard.core.service.CardServiceScheduler;
import opencard.core.service.SmartCard;
import opencard.core.terminal.CardID;
import opencard.core.terminal.ResponseAPDU;
import opencard.core.util.HexString;
import opencard.opt.service.CardServiceObjectNotAvailableException;
import opencard.opt.terminal.ISOCommandAPDU;

/**
 * The JavaCardCCProxy provides a set of functions that the caller can
 * use to access the PurseCardService on a Smart card, in addition
 * to function to access CorporateCard services like the BusinessCard
 * store.
 * 
 * @author      Rinaldo Di Giorgio (rinaldo.digiorgio@Sun.com)
 * @author      Colin Stevens (colin.stevens@sun.com)
 * @version     1.22, 99/03/26
 * 
 * @see sunlabs.brazil.handler.SecureTokenDeviceHandler
 */
public class JavaCardCCProxyService extends CardService implements PurseCardService {
    private static final String corporateCardName = "CorporateCard";
    private static final byte[] aidGemPlus = {
        (byte) 0xa0, 0, 0, 0, (byte) 0x18, (byte) 0xff, 0, 0, 0, 0, 0, 0, 0, 
        0, 0, 1
    };
    final static byte BZ = (byte) 0x00;
    final static byte BFF = (byte) 0xFF;

    // Main CLA supported by the CorporateCard applet

    final static byte CC_CLA = (byte) 0x80;
    final static byte SHA1_HASH = (byte) 0x80;
    final static byte XOR_HASH = (byte) 0x82;
    final static byte XOR_CHALLENGE_RESPONSE = (byte) 0x50;
    final static byte XOR_VERIFY_RESPONSE = (byte) 0x52;
    final static byte SHA1_CHALLENGE_RESPONSE = (byte) 0x54;
    final static byte SHA1_VERIFY_RESPONSE = (byte) 0x4E;

    // P2 byte for Master PIN in Verify

    final static byte USER_PIN = (byte) 0x82;
    final static byte USER_PIN_LENGTH = (byte) 8;

    // INS --Business Card Instructions supported by the CorporateCard applet

    final static byte CC_STORE = (byte) 0x3C;
    final static byte CC_GETSIZE = (byte) 0x3E;
    final static byte CC_RETRIEVE = (byte) 0x40;
    final static byte CC_RETRIEVE_NEXT = (byte) 0x42;

    // Paramters for CC_INS_STORE

    final static byte CC_P1_SET_LENGTH = (byte) 0x01;
    final static byte CC_P1_NEXT_PACKET = (byte) 0x02;
    final static byte CC_P1_LAST_PACKET = (byte) 0x03;
    final static short CC_SIZE = (short) 0x1FE;
    final static short CC_STORE_SIZE = (short) 0x100;
    final static byte APDU_XFER_MAX = 40;
    static final byte NOT_DECIPHER = (byte) 0x5A;
    static final byte RC4_DECIPHER = (byte) 0x7E;
    static final byte HASH_SIZE = (byte) 0x14;
    private Hashtable cardInfo = 
        new Hashtable();        // Will fail for multiple cards XXX
    private Properties atrMap = new Properties();
    private boolean isSelected = false;
    private boolean verbose = true;
    String purseID;
    private static final JavaCardCCStatus ccStatus = new JavaCardCCStatus();

    /**
     * Perform class initializations if any.
     * 
     * 
     */
    public JavaCardCCProxyService() throws IOException {}

    /**
     * Implement the required Open Card method.
     * 
     * 
     * @param sched
     * @param card
     * @param block
     * 
     * @exception OpenCardException
     * 
     */
    protected void initialize(CardServiceScheduler sched, SmartCard card, 
                              boolean block) throws OpenCardException {
        super.initialize(sched, card, block);

        /*
         * Make sure that the application is really on the card.
         */
        try {
            setup();
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Helper procedure that must be called before sending a CorporateCard
     * APDU to the smartcard.  It allocates a CardChannel to talk to the
     * smartcard and ensures that the CorporateCard cardlet is present.
     * <p>
     * The CardChannel allocated by this function must eventually be
     * released by the caller.
     * 
     * @exception   OpenCardException
     * if there was an error selecting the CorporateCard.
     */
    private void setup() throws OpenCardException {
        allocateCardChannel();

        if (isSelected == false) {
            ISOCommandAPDU cmd;

            select();

            isSelected = true;
            cmd = new ISOCommandAPDU(CC_CLA, 0x56, 0x00, 0x00, 16);
            purseID = new String(execute(cmd));

            System.out.println("Select complete");
            cardInfo.put("id", purseID);
        }
    }

    /**
     * Sends an APDU and get a response APDU
     * 
     * 
     * @param cmd
     * 
     * @return
     * 
     * @exception OpenCardException
     * 
     */
    public ResponseAPDU sendAPDU(ISOCommandAPDU cmd) 
            throws OpenCardException {
        CardChannel chan = getCardChannel();
        ResponseAPDU resp = chan.sendCommandAPDU(cmd);

        if (resp.sw1() == (byte) 0x9f) {
            cmd = new ISOCommandAPDU(0x00, 0xC0, 0x00, 0x00, resp.sw2());
            resp = chan.sendCommandAPDU(cmd);
        }

        return resp;
    }

    /**
     * Execute a command APDU and return the response as byte[] for analysis.
     * 
     * 
     * @param cmd
     * 
     * @return
     * 
     * @exception OpenCardException
     * 
     */
    public byte[] execute(ISOCommandAPDU cmd) throws OpenCardException {
        int le = cmd.getLE();

        System.out.println("      In:" + cmd);

        ResponseAPDU resp = sendAPDU(cmd);

        System.out.println("Response:" + resp);
        ccStatus.check(resp.sw(), cmd);

        if ((le > 0) && (le != resp.getLength() - 2)) {
            ccStatus.check(0x6700 + le, cmd);
        }

        return resp.data();
    }

    /**
     * Gets the account id that is on the card.
     * See the PurseCardService documentation for further details.
     * 
     * @return      A string that represents the id.
     * 
     * @exception   OpenCardException
     * if there was an error talking to the card.
     * 
     * @implements  PurseCardService#purseId
     */
    public String purseId() throws OpenCardException {
        try {
            setup();

            return purseID;
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Gets information about the funds that are on the card.
     * See the PurseCardService documentation for further details.
     * 
     * @param       result
     * The funds information is stored in this hashtable.
     * 
     * @exception   OpenCardException
     * if there was an error talking to the card.
     * 
     * @implements  PurseCardService#purseQuantities
     */
    public void purseQuantities(Hashtable result) throws OpenCardException {
        try {
            setup();

            ISOCommandAPDU cmd = new ISOCommandAPDU(CC_CLA, 0x34, 0x00, 0x00, 
                                                    0x02);
            byte[] amt = execute(cmd);
            int uh = amt[0];
            int lh = amt[1];
            int amount = ((uh & 0xff) << 8) + (lh & 0xff);

            dbg("Amount in purse quantities:" + amount);
            result.put("id", purseID);
            result.put("currencies", "USD");
            result.put("USD.amount", Long.toString(amount));
            result.put("USD.minorPlaces", "2");
            result.put("USD.localCurrency", "DukeDollars");
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Gets a bag of information containing whatever properties this card
     * service wants to return.
     * See the PurseCardService documentation for further details.
     * 
     * @param       result
     * The information is stored in this hashtable.
     * 
     * @exception   OpenCardException
     * if there was an error talking to the card.
     * 
     * @implements  PurseCardService#purseInfo
     */
    public void purseInfo(Hashtable result) throws OpenCardException {
        try {
            setup();

            Enumeration e = cardInfo.keys();

            while (e.hasMoreElements()) {
                Object key = e.nextElement();

                result.put(key, cardInfo.get(key));
            }

            result.put("pinstate", getLockState());
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Sign the random challenge using the given message digest algorithm
     * to generate a digital signature.
     * See the PurseCardService documentation for further details.
     * 
     * @param       pin
     * The PIN, which should be the PIN for the card.
     * 
     * @param       challenge
     * Currently, this must be an array of 14 bytes to be
     * signed by the purse.
     * 
     * @param       digestType
     * The message digest algorithm name.
     * 
     * @return      The signature.
     * 
     * @exception   CardServiceOperationFailedException
     * if the wrong PIN was provided
     * 
     * @exception   CardServiceInabilityException
     * if an unsupported or invalid digest type was requested.
     * 
     * @exception   OpenCardException
     * if there was any other error talking to the card.
     * 
     * @implements  PurseCardService#purseCheckPin
     */
    public byte[] purseCheckPin(String pin, byte[] challenge, 
                                String digestType) throws OpenCardException {
        try {
            setup();

            ResponseAPDU resp;
            ISOCommandAPDU cmd;
            int ins = 0;

            if ((digestType == null) || digestType.equals("XOR")) {
                ins = XOR_CHALLENGE_RESPONSE;
            } else if (digestType.equals("SHA1")) {
                ins = SHA1_CHALLENGE_RESPONSE;
            } else {
                throw new CardServiceInabilityException("No support for:" 
                                                        + digestType);
            }

            byte[] buf = new byte[15];

            System.arraycopy(pin.getBytes(), 0, buf, 0, 
                             Math.min(pin.length(), 8));
            System.arraycopy(challenge, 0, buf, 8, 
                             Math.min(challenge.length, 7));

            cmd = new ISOCommandAPDU(CC_CLA, ins, USER_PIN_LENGTH, USER_PIN, 
                                     buf, HASH_SIZE);

            return execute(cmd);
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Uses the signature produced by <code>purseCheckPin</code> to
     * check whether the cardholder knew the pin of the purse and to
     * verify the purse id.
     * 
     * @param       purse
     * The purseID aka card number which has been signed
     * 
     * @param       challenge
     * The same challenge that was sent to the other purse.
     * 
     * @param       digestType
     * The message digest algorithm used to sign the challenge.
     * 
     * @param       signature
     * The signature generated by the purse.
     * 
     * @exception   CardServiceOperationFailedException
     * if the signature was not verified.
     * 
     * @exception   OpenCardException
     * if there was any other error talking to the card.
     * 
     * @implements  PurseCardService#purseVerifyPin
     */
    public byte[] purseVerifyPin(byte purse[], byte[] challenge, 
                                 String digestType, 
                                 byte[] signature) throws OpenCardException {
        byte[] input = new byte[challenge.length + purse.length];

        System.arraycopy(challenge, 0, input, 0, challenge.length);
        System.arraycopy(purse, 0, input, challenge.length, purse.length);
        System.out.print("Validate this Signature:");
        System.out.println(ByteArray.toHexString(signature));
        System.out.print("For this data:");
        System.out.println(ByteArray.toHexString(input));

        try {
            setup();

            ResponseAPDU resp;
            ISOCommandAPDU cmd;
            int ins = 0;

            if ((digestType == null) || digestType.equals("XOR")) {
                ins = XOR_VERIFY_RESPONSE;
            } else if (digestType.equals("SHA1")) {
                ins = SHA1_VERIFY_RESPONSE;
            } else {
                throw new CardServiceInabilityException("No support for:" 
                                                        + digestType);
            }

            cmd = new ISOCommandAPDU(CC_CLA, ins, 0, 0, input, HASH_SIZE);

            byte data[] = execute(cmd);

            if (!ByteArray.equals(data, signature)) {
                throw new CardServiceException("Signatures do not match");
            }

            return purse;
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Perform a value transfer eitehr upload or download.
     * 
     * 
     * @param args
     * @param result
     * 
     * @exception OpenCardException
     * 
     */
    public void purseValueTransfer(Hashtable args, Hashtable result) 
            throws OpenCardException {
        StringBuffer sb = new StringBuffer();
        String currencySymbol = null;
        String actionString = null;
        String amtRequested = null;
        boolean credit, debit;
        int step = 0;
        long amount;

        if (!args.containsKey("Step")) {
            throw new CardServiceException("Step not specified");
        }

        step = Integer.parseInt((String) args.get("Step"));

        if (!args.containsKey("TransactionData") 
                && (1 != step && 2 != step)) {
            throw new CardServiceException("TransactionData not specified");
        }

        switch (step) {

        case 1: {

            // Step one is always done on the server for this simple scheme

            result.put("error", "out of sequence");

            break;
        }

        case 2: {
            System.out.println("JavaCardReal:" + args);

            currencySymbol = (String) args.get("CurrencySymbol");
            actionString = (String) args.get("Action");
            amtRequested = (String) args.get("Amount");
            amount = Long.parseLong(amtRequested);

            if (actionString == null || currencySymbol == null 
                    || amtRequested == null) {
                result
                    .put("error", 
                         "Action, Amount and CurrencySymbol not specified for step 1");

                return;
            }

            credit = (actionString.toLowerCase().compareTo("credit") == 0);
            debit = (actionString.toLowerCase().compareTo("debit") == 0);

            if (!(credit ^ debit)) {
                result.put("error", "Step 2 is not a credit or debit");

                return;
            }
            if (!args.containsKey("Pin")) {
                result.put("error", "Pin Required");

                return;
            }

            byte pin[] = ((String) args.get("Pin")).getBytes();

            if (credit) {
                purseDeposit(pin, (short) amount);
            } else {
                purseWithDrawal(pin, (short) amount);
            }

            // Success or failure
            // Look at this and use proper signatures and secrets

            Long d = new Long((long) (Math.random() * 1000000.00));
            String tid = d.toString();
            String id = (String) args.get("id");

            result.put("Step", "3");
            result.put("Signature", tid);
            result.put("id", id);

            break;
        }

        default: {
            result.put("error", "Unknown step");
        }
        }
    }

    /**
     * Deposit money on to the card.
     * 
     * 
     * @param corporateCardPin
     * @param depositAmount
     * 
     * @exception OpenCardException
     * 
     */
    private void purseDeposit(byte[] corporateCardPin, 
                              short depositAmount) throws OpenCardException {
        try {
            setup();
            transfer(0x30, corporateCardPin, depositAmount);
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Withdraw some money from the card.
     * 
     * 
     * @param corporateCardPin
     * @param withDrawalAmount
     * 
     * @exception OpenCardException
     * 
     */
    private void purseWithDrawal(byte[] corporateCardPin, short withDrawalAmount) 
            throws OpenCardException {
        try {
            setup();
            transfer(0x7C, corporateCardPin, withDrawalAmount);
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Transfer funds.
     * 
     * 
     * @param cCode
     * @param ccPin
     * @param depAmount
     * 
     * @exception OpenCardException
     * 
     */
    private void transfer(int cCode, byte[] ccPin, 
                          short depAmount) throws OpenCardException {
        ISOCommandAPDU cmd = new ISOCommandAPDU(CC_CLA, cCode, 
                                                (byte) ccPin.length, 0x82, 
                                                ccPin.length + 2, 2);

        cmd.append(ccPin);
        cmd.append((byte) (depAmount >> 8));
        cmd.append((byte) depAmount);
        execute(cmd);
    }

    /**
     * Hash the Data using the algorithm specified.
     * 
     * 
     * @param data -- data to be hashed
     * @param hashType the type of SHA1, XOR, MD2
     * 
     * @return the hash
     * 
     * @exception OpenCardException other more general OpenCard exceptions
     * 
     * @see http://www.rsa.com for more information on MD2
     * @see FIPS PUB 180-1
     */
    public byte[] hash(byte data[], 
                       String hashType) throws OpenCardException {
        byte hash[] = null;
        ISOCommandAPDU cpc;
        byte ins;

        if (hashType.equals("SHA1")) {
            ins = SHA1_HASH;
        } else if (hashType.equals("XOR")) {
            ins = XOR_HASH;
        } else if (hashType.equals("RSA")) {
            String error = "Encryption type not supported";

            throw new CardServiceOperationFailedException(error);
        } else {
            String error = "Encryption type not supported";

            throw new CardServiceOperationFailedException(error);
        }

        byte len = (byte) data.length;

        cpc = new ISOCommandAPDU(CC_CLA, ins, 0x00, 0x00, data, HASH_SIZE);

        try {
            setup();

            return (execute(cpc));
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Decipher the data using the specified algorithm.
     * 
     * 
     * @param data -- data to be encrypted.
     * @param cipherType the type of cipher, NOT, RC4
     * 
     * @return the decrypted input data
     * 
     * @exception CardServiceOperationFailedException cipherType not supported
     * @exception OpenCardException other more general OpenCard exceptions
     * 
     * @see http://www.rsa.com for more information on RC4
     */
    public byte[] decipher(byte data[], 
                           String cipherType) throws OpenCardException {
        ISOCommandAPDU cpc;
        byte ins;

        if (cipherType.equals("NOT")) {
            ins = NOT_DECIPHER;
        } else if (cipherType.equals("RC4")) {
            ins = RC4_DECIPHER;
        } else {
            String error = "Encryption type not supported";

            throw new CardServiceOperationFailedException(error);
        }

        byte len = (byte) data.length;

        cpc = new ISOCommandAPDU(CC_CLA, ins, 0x00, 0x00, data, len);

        try {
            setup();

            return (execute(cpc));
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Encipher the data using the specified algorithm.
     * 
     * 
     * @param data -- data to be encrypted.
     * @param cipherType the type of cipher, NOT, RC4
     * 
     * @return the decrypted input data
     * 
     * @exception CardServiceOperationFailedException cipherType not supported
     * @exception OpenCardException other more general OpenCard exceptions
     * 
     * @see http://www.rsa.com for more information on RC4
     */
    public byte[] encipher(byte data[], 
                           String cipherType) throws OpenCardException {
        String error = "Encipher Unsupported Due to space limitations:";

        throw new CardServiceOperationFailedException(error + cipherType);
    }

    /**
     * Get the Application Identifier.
     * 
     * 
     * @param appName
     * 
     * @return
     * 
     * @exception CardServiceObjectNotAvailableException
     * 
     */
    private byte[] getAid(String appName) 
            throws CardServiceObjectNotAvailableException {
        CardID cardID = getCard().getCardID();
        String atr = (ByteArray.toHexString(getATR())).toUpperCase();

        cardInfo.put("atr", HexString.hexify(getATR()));

        // First look the string up

        String cardType = System.getProperty("OpenCard.atr." + atr);

        System.out.println("The cardType is:" + cardType);

        if (cardType == null) {
            throw new CardServiceObjectNotAvailableException();
        }

        String aid = System.getProperty("OpenCard.aid." + appName);

        if (aid == null) {
            throw new CardServiceObjectNotAvailableException("Check Config file");
        }

        cardInfo.put("cardType", cardType);

        if (cardType.equals("JavaiButton") 
                || cardType.equals("JavaOneRing")) {
            aid = aid.substring(0, 30);
        }

        return (ByteArray.parseHexString(aid));
    }

    /**
     * Select the device using an AID.
     * XXX the Dallas parts cannot handle an AID  that is 16 bytes
     * long so we truncate it.  The Schlumberger card also has a
     * similar problem but it is related to uniqueness.
     */
    private void select() throws OpenCardException {
        dbg("Selecting by AID:");

        try {
            sendSelect((byte) 0x00, getAid("CorporateCard"));
            System.out.println("NON CRYPTO CARD SELECTED");
        } catch (Exception noCorporateCard) {
            try {
                sendSelect((byte) 0x00, getAid("CorporateCardSha1"));
                System.out.println("CRYPTO CARD SELECTED");
            } catch (Exception noCorporateCardSha1) {
                throw new CardServiceObjectNotAvailableException("NO CC/SHA1");
            }
        }
    }

    /**
     * Send a select sequence.
     * 
     * 
     * @param cla
     * @param sD
     * 
     * @exception OpenCardException
     * 
     */
    private void sendSelect(byte cla, byte sD[]) throws OpenCardException {
        dbg("Selecting Application");
        execute(new ISOCommandAPDU(cla, 0xa4, 0x04, 0x00, sD, -1));
    }

    /**
     * Read the contents of the secure personal storage area
     */
    public byte[] readCC() throws Exception {
        try {
            setup();

            ISOCommandAPDU cmd;
            byte[] data;

            cmd = new ISOCommandAPDU(CC_CLA, CC_GETSIZE, 0x00, 0x00, 2);
            data = execute(cmd);

            int sizeBCard = ((data[0] & 0xff) << 8) | (data[1] & 0xff);

            if (sizeBCard == 0) {
                return null;
            }

            dbg("Read: " + sizeBCard + " bytes from card.");

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int nBlocks = sizeBCard / APDU_XFER_MAX;
            int lastBlock = sizeBCard % APDU_XFER_MAX;
            int blockNumber = 0;

            /*
             * This seems inconvenient.  Perhaps it should be done using a
             * CASE 2E APDU?
             */
            cmd = new ISOCommandAPDU(CC_CLA, CC_RETRIEVE, APDU_XFER_MAX, 
                                     0x00, APDU_XFER_MAX);

            do {
                out.write(execute(cmd));
                cmd.setByte(ISOCommandAPDU.INS, CC_RETRIEVE_NEXT);
            } while (++blockNumber < nBlocks);

            if (lastBlock > 0) {
                cmd = new ISOCommandAPDU(CC_CLA, CC_RETRIEVE_NEXT, 
                                         (byte) lastBlock, 0x00, lastBlock);

                out.write(execute(cmd));
            }

            dbg("Returning buffer of size: " + out.size());

            return (out.toByteArray());
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Write business card fields to the BusinessCard applet on the iButton.
     */
    public void storeCC(byte[] bcData) throws Exception {
        try {
            setup();

            // Set the total length of the business card data

            byte[] buffer = new byte[2];

            buffer[0] = (byte) ((bcData.length & 0xFF00) >>> 8);
            buffer[1] = (byte) (bcData.length & 0xFF);

            // first APDU send tells the button what length of data to expect

            ISOCommandAPDU setLengthAPDU = new ISOCommandAPDU(CC_CLA, 
                    CC_STORE, CC_P1_SET_LENGTH, BZ, buffer, -1);
            ResponseAPDU rapdu = sendAPDU(setLengthAPDU);
            int offset = 0;
            int bytesRemaining = bcData.length;
            int packetBytes;

            // remaining APDU sends transmit data to the button in packets

            while (bytesRemaining > 0) {
                byte p1;

                if (bytesRemaining > APDU_XFER_MAX) {
                    packetBytes = APDU_XFER_MAX;
                    p1 = CC_P1_NEXT_PACKET;
                } else {
                    packetBytes = bytesRemaining;

                    // signals the final transmission

                    p1 = CC_P1_LAST_PACKET;
                }

                byte[] packetData = new byte[packetBytes];

                for (int j = 0; j < packetBytes; j++) {
                    packetData[j] = bcData[j + offset];
                }

                offset += packetBytes;

                ISOCommandAPDU storeAPDU = new ISOCommandAPDU(CC_CLA, 
                        (byte) CC_STORE, p1, BZ, packetData, (byte) -1);

                rapdu = sendAPDU(storeAPDU);
                bytesRemaining -= packetBytes;
            }
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Get the ATR for the current card.
     * 
     * 
     * @return
     * 
     */
    public byte[] getATR() {
        return ((getCard().getCardID()).getATR());
    }

    /**
     * Get Properties for this card.
     * 
     * 
     * @return
     * 
     */
    public Hashtable getCardProperties() {
        System.out.println(" returning card info: " + cardInfo);

        return (cardInfo);
    }

    /**
     * Debug support
     * 
     * 
     * @param s
     * 
     */
    public void dbg(String s) {
        if (verbose) {
            System.err.println(s);
        }
    }

    /**
     * Is changing of a pin allowed?
     */
    public boolean canChangePersonalCode() throws OpenCardException {
        return (true);

    // throw new CardServiceOperationFailedException("canChangePersonalCode");

    }

    /**
     * Changes the Personal Code
     * 
     * @param       currentPin
     * The PIN, which should be the PIN for the card.
     * 
     * @param       newPin
     * The new pin which is desired.
     * 
     * @exception   CardServiceOperationFailedException
     * if the wrong PIN was provided
     * 
     * @exception   OpenCardException
     * if there was any other error talking to the card.
     * @exception   OpenCardException
     * if the pin could not be changed for any reason
     * 
     * @implements  PurseCardService#changePersonalCode
     */
    public void changePersonalCode(String currentPin, 
                                   String newPin) throws OpenCardException {
        System.out.println(currentPin);
        System.out.println(newPin);

        try {
            setup();

            int cLen = currentPin.length();
            int nLen = newPin.length();
            ISOCommandAPDU cmd = new ISOCommandAPDU(CC_CLA, 0x38, BZ, BZ, 
                                                    cLen + nLen + 2, -1);

            cmd.append((byte) cLen);
            cmd.append(currentPin.getBytes());
            cmd.append((byte) nLen);
            cmd.append(newPin.getBytes());
            execute(cmd);
        }
        finally {
            releaseCardChannel();
        }
    }

    /**
     * Status of Pin locking.
     * 
     * 
     * @return
     * 
     * @exception OpenCardException
     * 
     */
    public String getLockState() throws OpenCardException {

        // XXX later check for locked out -- to many failues

        return ("Locked");
    }

    /**
     * Set a personal pin code
     * 
     * 
     * @param newCode
     * 
     * @exception OpenCardException
     * 
     */
    public void setPersonalCode(String newCode) throws OpenCardException {
        throw new CardServiceOperationFailedException("setPersonalCode");
    }

}

/**
 * An object for holding status information
 * 
 * 
 * 
 * @author
 * @version   1.23, 06/27/99
 */
class JavaCardCCStatus extends ISO7816Status {

    /**
     * Return error messages in a hash table
     * 
     * 
     * @return
     * 
     */
    public Hashtable getTable() {
        return msgs;
    }

    /*
     * Extend the standard set of ISO7816 errors.
     * the last niblle contains additional information in
     * some cases.
     */
    static final Hashtable msgs = new ChainHashtable(ISO7816Status.msgs);
    static {
        put(msgs, 0x9101, "invalid pin", Failed);
        put(msgs, 0x9102, "invalid pin", Failed);
        put(msgs, 0x9103, "insufficient funds", Failed);
        put(msgs, 0x9104, "deposit would exceed purse limit", Failed);
        put(msgs, 0x9105, "bad sequence", Invalid);
    }
}