/*
 * @(#)LoadCyberFlexAccess.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.
 *
 */
package sunlabs.oc.javacard.loaders.schlumberger;

import sunlabs.oc.javacard.loaders.JavaCardLoaderProxy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Hashtable;
import opencard.core.service.CardService;
import opencard.core.service.CardServiceRuntimeException;
import opencard.core.terminal.CardTerminalException;
import sun.oc.terminal.ISOCommandAPDU;
import opencard.core.terminal.CommandAPDU;
import opencard.core.terminal.ResponseAPDU;
import sunlabs.sts.ByteArray;
import sunlabs.oc.ErrorMsg;

/**
 * Load a CyberFlex Access card.
 * 
 * This class loads a CyberFlex Access card given a ByteInputStream
 * and a hashtable with load properties.  The
 * properties currently supported are:
 * 
 * <br>fileIdContainerSize -- size in bytes to make container
 * <br>instanceDataSize -- size in bytes to make instance
 * <br>fileId -- the ID for the file
 * <br>instanceFileId -- the id for the instance
 * <br>aid -- an aid to indentify this application
 * <br>appletDESKey -- the DES signature of the cardlets bytes for verification.
 * 
 * 
 * @author      Rinaldo Di Giorgio
 * @author      Mike Montgomery
 * @author      Jennifer Yonemitsu
 * @version   %I%, %G%
 */
public class LoadCyberFlexAccess extends CardService implements JavaCardLoaderProxy {
    private int tNumber = 0;
    private final byte APPLET = 0x01;
    private short fileIdContainerSize = 7000;
    private short instanceDataSize = 6000;
    private short fileId = 0x2222;
    private short instanceFileId = 0x2223;
    private byte aid[] = {
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
    };
    private byte appletDESKey[] = null;
    private byte BZ = (byte) 0;
    private byte B1 = (byte) 1;
    private byte B8 = (byte) 8;
    private byte FF = (byte) 0xFF;
    private byte SEL = (byte) 0xA4;
    private byte DEL = (byte) 0xE4;
    private byte SID = (byte) 0x02;     // Use file ID
    private int appletSize = 0;
    private short offset = 0;
    private String status = "Ready To Load";
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    private final byte selectRoot[] = {
        BZ, SEL, BZ, BZ, SID, (byte) 0x3F, BZ
    };
    private final byte selectFileId[] = {
        BZ, SEL, BZ, BZ, SID, (byte) 0x22, (byte) 0x22
    };
    private final byte selectInstance[] = {
        BZ, SEL, BZ, BZ, SID, (byte) 0x22, (byte) 0x23
    };
    private final byte deleteFileId[] = {
        BZ, DEL, BZ, BZ, SID, (byte) 0x22, (byte) 0x22
    };
    private final byte deletecDir[] = {
        BZ, DEL, (byte) 0x80, BZ, BZ
    };
    private final byte verifyKey[] = {
        BZ, (byte) 0x2A, BZ, BZ, (byte) 0x08, (byte) 0xAD, (byte) 0x9F, 
        (byte) 0x61, (byte) 0xFE, (byte) 0xFA, (byte) 0x20, (byte) 0xCE, 
        (byte) 0x63
    };
    private final byte nonCryptoKey[] = {
        (byte) 0xCE, (byte) 0xA4, (byte) 0x5D, (byte) 0xC2, (byte) 0xAE, 
        (byte) 89, (byte) 0xFE, (byte) 0x4F
    };
    private final byte createFileId[] = {
        BZ, (byte) 0xE0, BZ, BZ, (byte) 0x10, (byte) 0x10, (byte) 0x00, 
        (byte) 0x22, (byte) 0x22, (byte) 0x03, (byte) 0x01, BZ, BZ, FF, FF, 
        FF, FF, FF, FF, FF, FF
    };
    private final byte selectDefaultLoader[] = {
        BZ, SEL, (byte) 0x04, BZ
    };
    private final byte resetProgram[] = {
        BZ, (byte) 0x0a, (byte) 0x02, BZ
    };
    private final byte validateAppletDES[] = {
        BZ, (byte) 0x0a, B1, BZ, B8
    };
    private final byte validateAppletRSA[] = {
        BZ, (byte) 0x0a, B1, BZ, (byte) 128
    };
    private final byte createInstance[] = {
        BZ, (byte) 0x0C, (byte) 0x13, BZ, 0x1B
    };
    private final byte resetInstance[] = {
        BZ, (byte) 0x08, (byte) 0x03, BZ
    };
    private final byte deleteInstance[] = {
        BZ, (byte) 0x08, (byte) 0x02, BZ
    };
    private String containerID;
    private String prefix = "";

    /**
     * Loads the cardlet with the given properties.
     * 
     * 
     * @param loadProps Properties to be used in laoding this cardlet.
     * @param bis the bytes comprising the cardlet.
     * 
     * @return a hashtabnle with information about the load attempt.
     * 
     */
    public Hashtable downloadApplet(Hashtable loadProps, 
                                    ByteArrayInputStream bis) {
        appletSize = bis.available();
        offset = 0;
        status = "Ready To Load";

        String s = (String) loadProps.get("LOADERPREFIX");

        if (s != null) {
            prefix = s;
        }

        System.out.println("Starting download with Properties:" + loadProps);
        System.out.println("Starting download looking for prefixes:" 
                           + prefix);

        ResponseAPDU resp;
        Hashtable results = new Hashtable();

        // 
        // Get all the parameters from the user to overried current
        // Parameters
        // 

        try {
            fileIdContainerSize = getParamShort(loadProps, 
                                                "fileIdContainerSize");
        } catch (Exception e) {}
        ;
        try {
            instanceDataSize = getParamShort(loadProps, "instanceDataSize");
        } catch (Exception e) {}
        ;
        try {
            fileId = getParamShort(loadProps, "fileId");
        } catch (Exception e) {}
        ;
        try {
            instanceFileId = getParamShort(loadProps, "instanceFileId");
        } catch (Exception e) {}
        ;
        try {
            if (getParamByte(loadProps, "aid") != null) {
                aid = getParamByte(loadProps, "aid");
            }
        } catch (Exception e) {
            System.err
                .println("Error Loading AID Using 01020304050607089090A0B0C0D0E0F");
        }
        try {
            appletDESKey = getParamByte(loadProps, "appletDESKey");
        } catch (Exception e) {
            System.err.println("Error Loading DES Key");
        }

        System.out.println("fileIdContainerSize:" + fileIdContainerSize);
        System.out.println("instanceDataSize:" + instanceDataSize);
        System.out.println("fileId:" + fileId);
        System.out.println("instanceFileId:" + instanceFileId);
        System.out.println("Aid:" + ByteArray.toHexString(aid));

        if (appletDESKey != null) {
            System.out.println("appletDESKey:" 
                               + ByteArray.toHexString(appletDESKey));
        }

        try {
            status = "Preparing Directory Structure";

            allocateCardChannel();

            resp = sendAPDU(selectDefaultLoader);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select Default Loader");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            // APDUs for downloading and verifying a program.
            // Step 1:
            // Select the card root 3F00

            resp = sendAPDU(selectRoot);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select Root");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            // Step 2:
            // Verify yourself to the card (Key 0)

            resp = sendAPDU(verifyKey);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Authenticate");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            // Select the File ID and delete it

            resp = sendAPDU(selectFileId);

            if (resp.sw() == 0x9000) {

                // reset the program in case previous load failed
                // and program was left in an unknwon state.

                resp = sendAPDU(resetProgram);
                resp = sendAPDU(deleteFileId);

                if (resp.sw() != 0x9000) {
                    results.put("error", "Unable to Delete existing file");
                    results.put("errorData", resp);

                    status = "Load Error:" + resp;

                    return (results);
                }
            }

            // Select the File ID and delete it

            resp = sendAPDU(selectInstance);

            if (resp.sw() == 0x9000) {

                // reset the program in case previous load failed
                // and program was left in an unknwon state.

                resp = sendAPDU(resetInstance);
                resp = sendAPDU(deleteInstance);

                if (resp.sw() != 0x9000) {
                    results.put("error", 
                                "Unable to Delete existing Instance");
                    results.put("errorData", resp);

                    status = "Load Error:" + resp;

                    return (results);
                }
            }

            resp = sendAPDU(selectRoot);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select Root");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            resp = sendAPDU(createFileId);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Create File");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            // Select file

            resp = sendAPDU(selectFileId);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select File");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            resp = sendAPDU(selectDefaultLoader);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select Default Loader");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            resp = sendAPDU(resetProgram);

            // Step 6
            // Download the processed class to file 2222 as follows:
            // P1 and P2 form a 16-bit offset in the file; P1 is the most significant
            // byte, P2 is the least.  Le is the length of the block you are sending.  I
            // use a block size of 120 bytes of data.  Just use something convenient, such
            // as 100, 120, or 128.  Remember that the larger the size, the fewer APDUs
            // are needed, and the faster the download will be.  I would not go above 128,
            // though, since this might cause buffering problems.
            // A0 D6 P1 P2 Le   XX XX ...
            // Keep sending APDUs as necessary to get the entire file downloaded.

            int blockSize = 120;
            ByteArrayOutputStream xmitBuffer = new ByteArrayOutputStream();
            long fullBlocks = appletSize / blockSize;
            long lastBlock = appletSize % blockSize;

            System.out.println("The size of the applet: " + appletSize);
            System.out.println("The number of full blocks is: " + fullBlocks);
            System.out.println("The bytes in the last block: " + lastBlock);

            offset = 0;

            int blockNumber = 0;
            byte dataBlock[] = new byte[blockSize];

            status = "Transmitting Program";

            while (fullBlocks-- > 0) {
                xmitBuffer.reset();
                xmitBuffer.write(BZ);
                xmitBuffer.write(0xD6);
                xmitBuffer.write(left(offset));
                xmitBuffer.write(right(offset));
                xmitBuffer.write(blockSize);

                int amtread = bis.read(dataBlock, 0, blockSize);

                xmitBuffer.write(dataBlock, 0, blockSize);
                System.out.println("Sending block: " + blockNumber++);

                ResponseAPDU rapdu = sendAPDU(xmitBuffer.toByteArray());

                if (rapdu.sw() != 0x9000) {
                    results.put("error", rapdu.toString());

                    status = "Load Error:" + resp;

                    return (results);
                }

                offset += blockSize;
            }

            xmitBuffer.reset();
            xmitBuffer.write(BZ);
            xmitBuffer.write(0xD6);
            xmitBuffer.write(left(offset));
            xmitBuffer.write(right(offset));
            xmitBuffer.write((int) lastBlock);

            int amtread = bis.read(dataBlock, 0, blockSize);

            System.out.println("Sending last Block:");
            xmitBuffer.write(dataBlock, 0, (int) lastBlock);
            sendAPDU(xmitBuffer.toByteArray());

            offset += lastBlock;

            System.out.println("Validate Applet");

            status = "Validate Applet";

            xmitBuffer.reset();
            xmitBuffer.write(validateAppletDES, 0, 5);

            if (appletDESKey == null) {
                xmitBuffer.write(nonCryptoKey, 0, 8);
            } else {
                xmitBuffer.write(appletDESKey, 0, 8);
            }

            resp = sendAPDU(xmitBuffer.toByteArray());

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Authenticate Applet");
                results.put("errorData", resp);

                status = "Load Error:" + resp;
            }

            try {
                Thread.sleep(5 * 1000);
            } catch (Exception e) {}

            System.out.println("Select Root");

            resp = sendAPDU(selectRoot);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select Root");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            System.out.println("Select File");

            resp = sendAPDU(selectFileId);

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Select File");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            System.out.println("Create Instance");
            xmitBuffer.reset();
            xmitBuffer.write(createInstance, 0, 5);
            xmitBuffer.write(APPLET);
            xmitBuffer.write(left(fileId));
            xmitBuffer.write(right(fileId));
            xmitBuffer.write(left(fileIdContainerSize));
            xmitBuffer.write(right(fileIdContainerSize));
            xmitBuffer.write(left(instanceFileId));
            xmitBuffer.write(right(instanceFileId));
            xmitBuffer.write(left(instanceDataSize));
            xmitBuffer.write(right(instanceDataSize));
            xmitBuffer.write(BZ);
            xmitBuffer.write(0x10);
            xmitBuffer.write(aid, 0, 16);

            resp = sendAPDU(xmitBuffer.toByteArray());

            if (resp.sw() != 0x9000) {
                results.put("error", "Unable to Create Instance");
                results.put("errorData", resp);

                status = "Load Error:" + resp;

                return (results);
            }

            status = "Load Complete Loaded, Validated and Instance Created";
        } catch (CardTerminalException e) {
            results.put("error", ErrorMsg.format(e, null, "downloading"));
        }
        finally {
            try {
                bis.close();
            } catch (Exception e) {}
            try {
                releaseCardChannel();
            } catch (CardServiceRuntimeException e) {
                results.put("error", "card removed");

                return results;
            }
        }

        results.put("OK", getBytesSent() + " transferred.");

        return (results);
    }

    /**
     * Method to send an APDU and get the response
     * 
     * 
     * @param apdu to send.
     * 
     * @return the response APDU
     * 
     * @exception CardTerminalException is raised if the APDU could not be
     * send or received.
     * 
     */
    private ResponseAPDU sendAPDU(byte apdu[]) throws CardTerminalException {
        CommandAPDU commandAPDU = new CommandAPDU(apdu);

        System.out.println(">>>>>>>>>>>>>>>> Start Transaction Number " 
                           + ++tNumber);
        System.out.println(commandAPDU);

        ResponseAPDU responseAPDU = 
            getCardChannel().sendCommandAPDU(commandAPDU);

        System.out.println(responseAPDU);

        if (responseAPDU.sw1() == (byte) 0x61) {
            ISOCommandAPDU apduToSend = new ISOCommandAPDU((byte) 0x00, 
                    (byte) 0xC0, (byte) 0x00, (byte) 0x00, 
                    (int) responseAPDU.sw2());

            responseAPDU = getCardChannel().sendCommandAPDU(apduToSend);
        }

        System.out.println("<<<<<<<<<<<<<<<<   End Transaction Number " 
                           + tNumber);

        return (responseAPDU);
    }

    /**
     * Constructor method which can have additional initializations
     * 
     * 
     */
    public LoadCyberFlexAccess() {}

    /**
     * Get a short from the properties object
     * 
     * 
     * @param lp the hash table with the values
     * @param key the parameter we want
     * 
     * @return the short which is the parameter value
     * 
     */
    private short getParamShort(Hashtable lp, String key) {
        String s = (String) lp.get(prefix + key);

        return (Short.parseShort(s));
    }

    /**
     * Get a byte array from the properties object
     * 
     * 
     * 
     * @param lp the hash table with the values
     * @param key the parameter we want
     * 
     * @return the byte array which is the parameter value
     * 
     */
    private byte[] getParamByte(Hashtable lp, String key) {
        String s = (String) lp.get(prefix + key);
        byte[] data = ByteArray.parseHexString(s);

        return (data);
    }

    /**
     * Print debugging information for given object
     * 
     * 
     * @param o the object to use.
     * 
     */
    void debugObject(Object o) {
        System.out.println(o.toString());
    }

    /**
     * Shift a short left 8
     * 
     * 
     * @param s the short to shift left 8
     * 
     * @return the byte result of the shift
     * 
     */
    byte left(short s) {
        return ((byte) ((s & (short) 0xFF00) >> 8));
    }

    /**
     * Lower Byte of a short
     * 
     * 
     * @param s the short
     * 
     * @return byte with lower byte of short
     * 
     */
    byte right(short s) {
        return ((byte) ((s & (short) 0x00FF)));
    }

    /**
     * Write a short to a byte output stream
     * 
     * 
     * @param s the short
     * 
     */
    void stuffShort(short s) {
        baos.write(left(s));
        baos.write(right(s));
    }

    /**
     * Fill byte to a an output stream
     * 
     * 
     * @param b the fill byte
     * @param number the number of bytes to fill
     * 
     */
    void stuffBytes(byte b, int number) {
        int count = number;

        while (count-- > 0) {
            baos.write(b);
        }
    }

    /**
     * Number of bytes to send for this cardlet
     * 
     * 
     * @return the size of the cardlet
     * 
     */
    public int getBytesToSend() {
        return (appletSize);
    }

    /**
     * Number of bytes sent
     * 
     * 
     * @return the current number of bytes transmitted to the card.
     * 
     */
    public int getBytesSent() {
        return (offset);
    }

    /**
     * String representation of the load process.
     * 
     * 
     * @return the string representation of the current load state.
     * 
     */
    public String getStatus() {
        return (status);
    }

    /**
     * Reset this object for a new load to begin.
     * 
     * 
     */
    public void reset() {
        status = "Ready to Load";
        appletSize = 0;
        offset = 0;
    }

}