/**
 * Copyright (c) 1999 GEMPLUS group. All Rights Reserved.
 *------------------------------------------------------------------------------
 *  Project name:  GemXpresso Environment
 *                 - OPPurse a Javacard 2.1 & OP 2.0.1 applet example
 *
 *  Platform    :  Java virtual machine
 *  Language    :  JAVA 1.2
 *  Devl tool   :  Symantec VisualCafe 3.0C
 *
 *  Original author: Gemplus Java Card Group Software Environment
 *------------------------------------------------------------------------------
 */

/*
 * Package name
 */
package com.gemplus.examples.oppurse;

/*
 * Imported packages
 */
import javacard.framework.*;
import visa.openplatform.*;

public class OPPurse extends javacard.framework.Applet
{
    // the APDU constants for all the commands.
        private final static byte CLA_OPPURSE               = (byte)0x90;
        private final static byte CLA_OPPURSE_SM            = (byte)0x94;
	private final static byte INS_GET_BALANCE           = (byte)0x10;
	private final static byte INS_DEBIT 	            = (byte)0x12;
	private final static byte INS_CREDIT 	            = (byte)0x14;
	private final static byte INS_VERIFY_PIN            = (byte)0x16;
    // the OP/VOP specific instruction set for mutual authentication
	private final static byte CLA_INIT_UPDATE           = (byte)0x80;
	private final static byte INS_INIT_UPDATE           = (byte)0x50;
	private final static byte CLA_EXTERNAL_AUTHENTICATE = (byte)0x84;
	private final static byte INS_EXTERNAL_AUTHENTICATE = (byte)0x82;

    // the PIN validity flag
    private boolean validPIN = false;

    // SW bytes for PIN Failed condition
	// the last nibble is replaced with the number of remaining tries
	private final static short 	SW_PIN_FAILED = (short)0x63C0;

    // the illegal amount value for the exceptions.
    private final static short ILLEGAL_AMOUNT = 1;

    // the maximum balance in this purse.
    private static final short maximumBalance = 10000;

    // the current balance in this purse.
    private short balance;

    /* Security part of declarations */

    // the Security Object necessary to credit the purse
    private ProviderSecurityDomain securityObject = null;

    // the security channel number
    byte secureChannel = (byte)0xFF;

    // the authentication status
    private boolean authenticationDone = false;

    // authentication Enciphered of Maced ?
    private boolean enciphered = false;

    // the secure channel status
    private boolean channelOpened = false;

    // the previous working state
    private byte previousState = OPSystem.APPLET_PERSONALIZED;

    /**
     * Only this class's install method should create the applet object.
     */
    protected OPPurse(byte[] buffer, short offset, byte length)
    {
        // data offset is used for application specific parameter.
        // initialization with default offset (AID offset).
        short dataOffset = offset;
        byte V2 = 1;

        if(length > 9) {
            // Install parameter detail. Compliant with OP 2.0.1.

            // | size | content
            // |------|---------------------------
            // |  1   | [AID_Length]
            // | 5-16 | [AID_Bytes]
            // |  1   | [Privilege_Length]
            // | 1-n  | [Privilege_Bytes] (normally 1Byte)
            // |  1   | [Application_Proprietary_Length]
            // | 0-m  | [Application_Proprietary_Bytes]

            // shift to privilege offset
            dataOffset += (short)( 1 + buffer[offset]);
            // finally shift to Application specific offset
            dataOffset += (short)( 1 + buffer[dataOffset]);
            // checks wrong data length
            if(buffer[dataOffset] != 2)
                // return received proprietary data length in the reason
                ISOException.throwIt((short)(ISO7816.SW_WRONG_LENGTH + offset + length - dataOffset));

            // go to proprietary data
            dataOffset++;
            V2 = 2;
        }
        else {
            // Install parameter compliant with OP 2.0.
            if(length != 2)
                ISOException.throwIt((short)(ISO7816.SW_WRONG_LENGTH + length));
        }
		// retreive the balance value from the APDU buffer
        short value = (short)(((buffer[(short)(dataOffset + 1)]) & 0xFF)
		                    | ((buffer[dataOffset] & 0xFF) << 8));

        // checks initial balance value
        if(value > maximumBalance)
            ISOException.throwIt(ISO7816.SW_DATA_INVALID);

		// initializes the balance with the APDU buffer contents
        balance = value;

        // register this instance as an installed Applet
	if(V2 == 2)
        	register(buffer, (short)(offset + 1), (byte)buffer[offset]);
        else
        	register();

        // ask the system for the Security Object associated to the Applet
        securityObject = OPSystem.getSecurityDomain();

        // applet is personalized and its state can change
        OPSystem.setCardContentState(OPSystem.APPLET_PERSONALIZED);

        // build the new ATR historical bytes
        byte[] newATRHistory = new byte[]
        {
            // put "OPPurse" in historical bytes.
            (byte)0x4F, (byte)0x50, (byte)0x50, (byte)0x75, (byte)0x72, (byte)0x73, (byte)0x65
        };
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!! ACTIVATED IF INSTALL PRIVILEGE IS "Default Selected" (0x04). !!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // change the default ATR to a personalized's one
        OPSystem.setATRHistBytes(newATRHistory, (short)0, (byte)newATRHistory.length);
    }

    /**
     * Method installing the applet.
     * <p>
     * Install parameter detail. Compliant with OP 2.0.1.<p>
     *<table border=1>
     *<tr><td>size</td><td>content</td></tr>
     *<tr><td>1</td><td>[AID_Length]</td></tr>
     *<tr><td>5-16</td><td>[AID_Bytes]</td></tr>
     *<tr><td>1</td><td>[Privilege_Length]</td></tr>
     *<tr><td>1-n</td><td>[Privilege_Bytes] (normally 1Byte)</tr></tb>
     *<tr><td>1</td><td>[Application_Proprietary_Length]</td></tr>
     *<tr><td>0-m</td><td>[Application_Proprietary_Bytes]</td></tr></table>
     * @param installparam the array constaining installation parameters
     * @param offset the starting offset in installparam
     * @param length the length in bytes of the data parameter in installparam
     */
    public static void install(byte[] installparam, short offset, byte length )
    throws ISOException
    {


        // applet  instance creation with the initial balance
        new OPPurse(installparam, offset, length );
    }

    /**
     * Select method returning true if applet selection is supported.
     * @return boolean status of selection.
     */
    public boolean select()
    {
        // reset security if used.
        // In case of reset deselect is not called
        reset_security();
        // return status of selection
        return true;
    }

    /**
     * Deselect method.
     */
    public void deselect()
    {
        // reset security if used.
        reset_security();
        return;
    }

    /**
     * Method processing an incoming APDU.
     * @see APDU
     * @param apdu the incoming APDU
     * @exception ISOException with the response bytes defined by ISO 7816-4
     */
    public void process(APDU apdu) throws ISOException
    {
        // get the APDU buffer
        // the APDU data is available in 'apduBuffer'
        byte[] apduBuffer = apdu.getBuffer();

        if( authenticationDone &&
            ((apduBuffer[ISO7816.OFFSET_CLA] & 0x0F) == 4))
          enciphered = true;
        else
          enciphered = false;

        // the "try" is mandatory because the debit method
        // can throw a javacard.framework.UserException
        try
        {
    	    switch(apduBuffer[ISO7816.OFFSET_INS])
    	    {
                case INS_VERIFY_PIN :
                        if( apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE ||
                            apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM)
                          verifyPIN(apdu);
                        else
                          ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case INS_GET_BALANCE :
                        if( apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE ||
                            apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM)
                          getBalance(apdu) ;
                        else
                          ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case INS_DEBIT :
                        if( apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE ||
                            apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM)
                          debit(apdu) ;
                        else
                          ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case INS_CREDIT :
                        if( apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE ||
                            apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM)
                          credit(apdu) ;
                        else
                          ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case INS_INIT_UPDATE :
                    if(apduBuffer[ISO7816.OFFSET_CLA] == CLA_INIT_UPDATE)
                        // call initialize/update security method
        	            init_update(apdu) ;
                    else
                        // wrong CLA received
                        ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case INS_EXTERNAL_AUTHENTICATE :
                    if(apduBuffer[ISO7816.OFFSET_CLA] == CLA_EXTERNAL_AUTHENTICATE)
                        // call external/authenticate security method
            	        external_authenticate(apdu) ;
                    else
                        // wrong CLA received
                        ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
                break ;

                case ISO7816.INS_SELECT :
                break ;

                default :
                    // The INS code is not supported by the dispatcher
        	        ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED) ;
                break ;
    	    }	// end of the switch
        } // end of the try
		catch(UserException e)
        {
            // translates the UserException in an ISOException.
		    if(e.getReason() == ILLEGAL_AMOUNT)
                throw new ISOException ( ISO7816.SW_DATA_INVALID ) ;
		}
    }

    //-------------------------------------------------------------------------
    //-               P R I V A T E     M E T H O D S                         -
    //-------------------------------------------------------------------------

	/**
	 * Handles Verify Pin APDU.
	 *
	 * @param apdu APDU object
	 */
    private void verifyPIN(APDU apdu)
	{
        // get APDU data
	apdu.setIncomingAndReceive();

        // Decrypt APDU buffer
        unwrapAPDU(apdu);
        // get APDU buffer
        byte[] apduBuffer = apdu.getBuffer();
        // check that the PIN is not blocked
        if((OPSystem.getTriesRemaining() == 0)
        && (OPSystem.getCardContentState() != OPSystem.APPLET_BLOCKED)) {
            previousState = OPSystem.getCardContentState();
            OPSystem.setCardContentState(OPSystem.APPLET_BLOCKED);
        }
        // Pin format for OP specification
        //
        // |type(2),length|nible(1),nible(2)|nible(3),nible(4)|...|nible(n-1),nible(n)|
        //
        // get Pin length
        byte length = (byte)(apduBuffer[ISO7816.OFFSET_LC] & 0x0F);
        // pad the PIN ASCII value
        for(byte i=length; i<0x0E; i++)
        {
            // only low nibble of padding is used
            apduBuffer[ISO7816.OFFSET_CDATA + i] = 0x3F;
        }
        // fill header TAG
        apduBuffer[0] = (byte)((0x02 << 4) | length);
        // parse ASCII Pin code
        for(byte i=0; i<0x0E; i++)
        {
            // fill bytes with ASCII Pin nibbles
            if((i & 0x01) == 0)
                // high nibble
                apduBuffer[(i >> 1)+1] = (byte)((apduBuffer[ISO7816.OFFSET_CDATA + i] & 0x0F) << 4);
            else
                // low nibble
                apduBuffer[(i >> 1)+1] |= (byte)(apduBuffer[ISO7816.OFFSET_CDATA + i] & 0x0F);
        }
        // verify the received PIN
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!! WARNING PIN HAS TO BE INITIALIZED BEFORE USE !!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        if(OPSystem.verifyPin(apdu, (byte)0))
        {
            // set PIN validity flag
            validPIN = true;
            // if applet state is BLOCKED then restore previous state (PERSONALIZED)
            if(OPSystem.getCardContentState() == OPSystem.APPLET_BLOCKED)
                OPSystem.setCardContentState(previousState);
            return;
		}
    	// the last nibble of returned code is the number of remaining tries
		ISOException.throwIt((short)(SW_PIN_FAILED + OPSystem.getTriesRemaining()));
	}

    /**
     * Performs the "getBalance" operation on this counter.
     *
     * @param apdu The APDU to process.
     */
    private void getBalance( APDU apdu )
    {
        // check valid Applet state
        if(OPSystem.getCardContentState() == OPSystem.APPLET_BLOCKED)
			ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);

        // get the APDU buffer
        byte[] apduBuffer = apdu.getBuffer();

        if(apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM)
          apdu.setIncomingAndReceive();

        // Decrypt APDU buffer
        unwrapAPDU(apdu);

        // writes the balance into the APDU buffer after the APDU command part
		apduBuffer[5] = (byte)(balance >> 8) ;
		apduBuffer[6] = (byte)balance ;

        // sends the APDU response
        // switches to output mode
		apdu.setOutgoing() ;
        // 2 bytes to return
		apdu.setOutgoingLength((short)2) ;
        // offset and length of bytes to return in the APDU buffer
		apdu.sendBytes((short)5, (short)2) ;
    }

    /**
     * Performs the "debit" operation on this counter.
     *
     * @param apdu The APDU to process.
     * @exception ISOException If the APDU is invalid.
     * @exception UserException If the amount to debit is invalid.
     */
    private void debit(APDU apdu) throws ISOException, UserException
    {
        // check valid Applet state
        if(OPSystem.getCardContentState() == OPSystem.APPLET_BLOCKED)
			ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);

        // the operation is allowed only if master pin is validated
	    if(!validPIN)
            ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);

        // get the APDU buffer
        byte[] apduBuffer = apdu.getBuffer();
        apdu.setIncomingAndReceive();

        // Decrypt APDU buffer
        unwrapAPDU(apdu);

        // Gets the length of bytes to recieved from the terminal and receives them
        // If does not receive 4 bytes throws an ISO.SW_WRONG_LENGTH exception
	if(apduBuffer[4] != 2) {
	    ISOException.throwIt(ISO7816.SW_WRONG_LENGTH) ;
        }

	// Reads the debit amount from the APDU buffer
        // Starts at offset 5 in the APDU buffer since the 5 first bytes
        // are used by the APDU command part
		short amount = (short)(((apduBuffer[6]) & (short)0x000000FF)
            | ((apduBuffer[5] << 8 ) & (short)0x0000FF00));

        // tests if the debit is valid
        if((balance >= amount) && (amount > 0))
        {
            // does the debit operation
            balance -= amount ;

            // writes the new balance into the APDU buffer
            // (writes after the debit amount in the APDU buffer)
            apduBuffer[7] = (byte)(balance >> 8) ;
            apduBuffer[8] = (byte)balance ;

            // sends the APDU response
            apdu.setOutgoing() ; // Switches to output mode
            apdu.setOutgoingLength((short)2) ; // 2 bytes to return
            // offset and length of bytes to return in the APDU buffer
            apdu.sendBytes((short)7, (short)2) ;
        }
        else
            // throw a UserException with illegal amount as reason
            throw new UserException(ILLEGAL_AMOUNT) ;
    }

    /**
     * Performs the "credit" operation on this counter. The operation is allowed only
     * if master pin is validated
     *
     * @param apdu             The APDU to process.
     * @exception ISOException If the APDU is invalid or if the amount to credit
     *                         is invalid.
     */
    private void credit(APDU apdu) throws ISOException
    {
        // check valid Applet state
        if(OPSystem.getCardContentState() == OPSystem.APPLET_BLOCKED)
			ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);

        // the operation is allowed only if master pin is validated and authentication is done
	    if (!validPIN || !authenticationDone)
            ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);

        // get the APDU buffer
        byte[] apduBuffer = apdu.getBuffer();
        apdu.setIncomingAndReceive();

        // Decrypt APDU buffer
        unwrapAPDU(apdu);

	// gets the length of bytes to recieved from the terminal and receives them
        // if does not receive 2 bytes throws an ISO.SW_WRONG_LENGTH exception
        if(apduBuffer[4] != 2)
            throw new ISOException(ISO7816.SW_WRONG_LENGTH);

		// reads the credit amount from the APDU buffer
        // starts at offset 5 in the APDU buffer since the 5 first bytes
        // are used by the APDU command part
		short amount = (short)(((apduBuffer[6]) & (short)0x000000FF)
            | ((apduBuffer[5] << 8) & (short)0x0000FF00));

        // tests if the credit is valid
        if(((short)(balance + amount) > maximumBalance) || (amount <= (short)0))
            throw new ISOException(ISO7816.SW_DATA_INVALID) ;
        else
            // does the credit operation
            balance += amount ;

        // writes the new balance into the APDU buffer
        // (writes after the credit amount in the APDU buffer)
        apduBuffer[7] = (byte)(balance >> 8) ;
        apduBuffer[8] = (byte)balance ;

        // sends the APDU response
        apdu.setOutgoing() ; // Switches to output mode
        apdu.setOutgoingLength((short)2) ; // 2 bytes to return
        // offset and length of bytes to return in the APDU buffer
        apdu.sendBytes((short)7, (short)2) ;
    }

    /**
     * Performs the "init_update" security operation.
     *
     * @param apdu The APDU to process.
     */
    private void init_update( APDU apdu )
    {
        // receives data
        apdu.setIncomingAndReceive();
        // checks for existing active secure channel
        if(channelOpened)
        {
            // close the openned security channel
            try
            {
                securityObject.closeSecureChannel(secureChannel);
            }
            catch(CardRuntimeException cre2)
            {
                // channel number is invalid. this case is ignored
            }
            // set the channel flag to close
            channelOpened = false;
        }
        try
        {
            // open a new security channel
            secureChannel = securityObject.openSecureChannel(apdu);
            // set the channel flag to open
            channelOpened = true;
            // get expected length
            short expected = apdu.setOutgoing();
            // send authentication result
            // expected length forced to 0x1C
            apdu.setOutgoingLength((byte)0x1C);
            apdu.sendBytes(ISO7816.OFFSET_CDATA, (byte)0x1c);
        }
        catch(CardRuntimeException cre)
        {
            // no available channel or APDU is invalid
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
    }

    /**
     * Performs the "external_authenticate" security operation.
     *
     * @param apdu The APDU to process.
     */
    private void external_authenticate( APDU apdu )
    {
        // receives data
        apdu.setIncomingAndReceive();
        // checks for existing active secure channel
        if(channelOpened)
        {
            try
            {
                // try to authenticate the client
                securityObject.verifyExternalAuthenticate(secureChannel, apdu);
                // authentication succeed
                authenticationDone = true;
            }
            catch(CardRuntimeException cre)
            {
                // authentication fails
                // set authentication flag to fails
                authenticationDone = false;
                // close the openned security channel
                try {
                    securityObject.closeSecureChannel(secureChannel);
                } catch(CardRuntimeException cre2) {
                    // channel number is invalid. this case is ignored
                }
                // set the channel flag to close
                channelOpened = false;
                // send authentication result
                ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
            }
            // send authentication result
            ISOException.throwIt(ISO7816.SW_NO_ERROR);
        }
        else
            ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
    }

    /**
     * The "reset_security" method close an opened secure channel if exist.
     * @return void.
     */
    public void reset_security()
    {
        // close the secure channel if openned.
        if(secureChannel != (byte)0xFF)
        {
            try
            {
                // close the openned security channel
                securityObject.closeSecureChannel(secureChannel);
            }
            catch(CardRuntimeException cre2)
            {
                // channel number is invalid. this case is ignored
            }
            // reset security parameters
            secureChannel = (byte)0xFF;
            channelOpened = false;
            // set validity flags to false
            validPIN = authenticationDone = false;
        }
        return;
    }

    /**
     * The "unwrapAPDU" method decrypt APDU buffer if needed (bit 0x04 up in instruction class).
     * @param APDU The apdu to decrypt
     *
     * @return void.
     */
    public void unwrapAPDU(APDU apdu)
    {
      byte[] apduBuffer = apdu.getBuffer();

      if(authenticationDone && (apduBuffer[ISO7816.OFFSET_CLA] == CLA_OPPURSE_SM))
        securityObject.unwrap(secureChannel, apdu);
    }
}
