/*
 * openjavacard-ndef: JavaCard applet implementing an NDEF tag
 * Copyright (C) 2015-2018 Ingo Albrecht <[email protected]>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.openjavacard.ndef.stub;

import javacard.framework.AID;
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.JCSystem;
import javacard.framework.Shareable;
import javacard.framework.Util;

/**
 * \brief Applet implementing an NDEF type 4 tag using a service
 *
 * This is the STUB variant of the applet.
 *
 * Implemented to comply with:
 *   NFC Forum
 *   Type 4 Tag Operation Specification
 *   Version 2.0
 *
 * Conformity remarks:
 *   1. The NDEF data file is restricted in size to the maximum
 *      size of initialization data on the specific platform.
 *   2. No file control information (FCI) is returned in SELECT responses
 *      as allowed by specification requirement RQ_T4T_NDA_034.
 *   3. Proprietary files are not being used.
 *
 */
public final class NdefApplet extends Applet {

    /* Instructions */
    private static final byte INS_SELECT        = ISO7816.INS_SELECT;
    private static final byte INS_READ_BINARY   = (byte)0xB0;
    private static final byte INS_UPDATE_BINARY = (byte)0xD6;

    /* File IDs */
    private static final short FILEID_NONE              = (short)0x0000;
    private static final short FILEID_NDEF_CAPABILITIES = (short)0xE103;
    private static final short FILEID_NDEF_DATA         = (short)0xE104;

    /* File access specifications */
    private static final byte FILE_ACCESS_OPEN = (byte)0x00;
    private static final byte FILE_ACCESS_NONE = (byte)0xFF;

    /* Parameters for SELECT */
    private static final byte SELECT_P1_BY_FILEID     = (byte)0x00;
    private static final byte SELECT_P2_FIRST_OR_ONLY = (byte)0x0C;

    /* NDEF mapping version (specification 2.0) */
    private static final byte NDEF_MAPPING_VERSION = (byte)0x20;

    /* Constants related to capability container */
    private static final byte CC_LEN_HEADER = 7;
    private static final byte CC_TAG_NDEF_FILE_CONTROL = 0x04;
    private static final byte CC_LEN_NDEF_FILE_CONTROL = 6;

    /**
     * Configuration: maximum read block size
     */
    private static final short NDEF_MAX_READ = 128;

    /**
     * Configuration: maximum write block size
     */
    private static final short NDEF_MAX_WRITE = 128;

    /**
     * Configuration: read access
     */
    private static final byte NDEF_READ_ACCESS = FILE_ACCESS_OPEN;

    /**
     * Configuration: write access
     */
    private static final byte NDEF_WRITE_ACCESS = FILE_ACCESS_NONE;

    /** Transient variables */
    private static short[] vars;
    /** Index for currently selected file */
    private static final byte VAR_SELECTED_FILE = (byte)0;
    /** Number of transient variables */
    private static final short NUM_VARS = (short)1;

    /** Transient references */
    private static Object[] refs;
    /** Index for reference to service */
    private static final byte REF_SERVICE = (byte)0;
    /** Index for reference to data */
    private static final byte REF_DATA = (byte)1;
    /** Number of transient references */
    private static final short NUM_REFS = (short)2;

    /** ID of the service */
    private static byte   serviceID;
    /** AID of the service */
    private static byte[] serviceAID;

    /** NDEF capability file contents */
    private static byte[] capsFile;

    /**
     * Installs an NDEF applet
     *
     * Will create, initialize and register an instance of
     * this applet as specified by the provided install data.
     *
     * Requested AID will always be honored.
     * Control information is ignored.
     * Applet data will be used for initialization.
     *
     * @param buf containing install data
     * @param off offset of install data in buf
     * @param len length of install data in buf
     */
    public static void install(byte[] buf, short off, byte len) {
        short pos = off;
        // find AID
        byte  lenAID = buf[pos++];
        short offAID = pos;
        pos += lenAID;
        // find control information (ignored)
        byte  lenCI = buf[pos++];
        short offCI = pos;
        pos += lenCI;
        // find applet data
        byte  lenAD = buf[pos++];
        short offAD = pos;
        pos += lenAD;

        // instantiate and initialize the applet
        NdefApplet applet = new NdefApplet(buf, offAD, lenAD);

        // register the applet
        applet.register();
    }

    /**
     * Main constructor
     *
     * This will construct and initialize an instance
     * of this applet according to the provided app data.
     *
     * @param buf containing application data
     * @param off offset of app data in buf
     * @param len length of app data in buf
     */
    protected NdefApplet(byte[] buf, short off, byte len) {
        // create transient variables
        vars = JCSystem.makeTransientShortArray(NUM_VARS, JCSystem.CLEAR_ON_DESELECT);
        refs = JCSystem.makeTransientObjectArray(NUM_REFS, JCSystem.CLEAR_ON_DESELECT);
        // create capabilities files
        capsFile = makeCaps((short)0);
        // process install data
        if(len < 6 || len > 17) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        // first byte is the service ID
        serviceID = buf[off++]; len--;
        // rest is the service AID
        serviceAID = new byte[len];
        Util.arrayCopyNonAtomic(buf, off, serviceAID, (short)0, len);
    }

    /**
     * Create and initialize the CAPABILITIES file
     *
     * @param dataSize to be allocated
     * @return an array for use as the CC file
     */
    private byte[] makeCaps(short dataSize) {
        short capsLen = (short)(CC_LEN_HEADER + 2 + CC_LEN_NDEF_FILE_CONTROL);
        byte[] caps = new byte[capsLen];

        short pos = 0;

        // CC length
        pos = Util.setShort(caps, pos,  capsLen);
        // mapping version
        caps[pos++] = NDEF_MAPPING_VERSION;
        // maximum read size
        pos = Util.setShort(caps, pos, NDEF_MAX_READ);
        // maximum write size
        pos = Util.setShort(caps, pos, NDEF_MAX_WRITE);

        // NDEF File Control TLV
        caps[pos++] = CC_TAG_NDEF_FILE_CONTROL;
        caps[pos++] = CC_LEN_NDEF_FILE_CONTROL;
        // file ID
        pos = Util.setShort(caps, pos, FILEID_NDEF_DATA);
        // file size
        pos = Util.setShort(caps, pos, dataSize);
        // read access
        caps[pos++] = NDEF_READ_ACCESS;
        // write access
        caps[pos++] = NDEF_WRITE_ACCESS;

        // check consistency
        if(pos != capsLen) {
            ISOException.throwIt(ISO7816.SW_UNKNOWN);
        }

        // return the file
        return caps;
    }

    /**
     * @return true if the stub is connected to a service
     */
    private boolean isConnected() {
        return refs[REF_SERVICE] != null && refs[REF_DATA] != null;
    }

    /**
     * Attempt to connect to the backend service
     */
    private void connectService() {
        NdefService service = null;
        // get AID object for service
        AID aid = JCSystem.lookupAID(serviceAID, (short)0, (byte)serviceAID.length);
        if(aid != null) {
            // get service object
            Shareable share = JCSystem.getAppletShareableInterfaceObject(aid, serviceID);
            // cast the service object
            if(share instanceof NdefService) {
                service = (NdefService)share;
            }
        }
        // check that we got a valid object
        if(service == null) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
        // retrieve the data array
        byte[] data = service.getData();
        if(data == null) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
        // remember both references
        refs[REF_SERVICE] = service;
        refs[REF_DATA] = data;
    }

    /**
     * Process an APDU
     *
     * This is the outer layer of our APDU dispatch.
     *
     * It deals with the CLA and INS of the APDU,
     * leaving the rest to an INS-specific function.
     *
     * @param apdu to be processed
     * @throws ISOException on error
     */
    public final void process(APDU apdu) throws ISOException {
        byte[] buffer = apdu.getBuffer();
        byte ins = buffer[ISO7816.OFFSET_INS];

        // handle selection of the applet
        if(selectingApplet()) {
            vars[VAR_SELECTED_FILE] = FILEID_NONE;
            connectService();
            return;
        }

        // if we are not connected then fail
        if(!isConnected()) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }

        // secure messaging is not supported
        if(apdu.isSecureMessagingCLA()) {
            ISOException.throwIt(ISO7816.SW_SECURE_MESSAGING_NOT_SUPPORTED);
        }

        // process commands to the applet
        if(apdu.isISOInterindustryCLA()) {
            if (ins == INS_SELECT) {
                processSelect(apdu);
            } else if (ins == INS_READ_BINARY) {
                processReadBinary(apdu);
            } else if (ins == INS_UPDATE_BINARY) {
                ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED);
            } else {
                ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
            }
        } else {
            ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
        }
    }

    /**
     * Process a SELECT command
     *
     * This handles only the one case mandated by the NDEF
     * specification: SELECT FIRST-OR-ONLY BY-FILE-ID.
     *
     * The file ID is specified in the APDU contents. It
     * must be exactly two bytes long and also valid.
     *
     * @param apdu to process
     * @throws ISOException on error
     */
    private void processSelect(APDU apdu) throws ISOException {
        byte[] buffer = apdu.getBuffer();
        byte p1 = buffer[ISO7816.OFFSET_P1];
        byte p2 = buffer[ISO7816.OFFSET_P2];

        // we only support what the NDEF spec prescribes
        if(p1 != SELECT_P1_BY_FILEID || p2 != SELECT_P2_FIRST_OR_ONLY) {
            ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED);
        }

        // receive data
        short lc = apdu.setIncomingAndReceive();

        // check length, must be for a file ID
        if(lc != 2) {
            ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
        }

        // retrieve the file ID
        short fileId = Util.getShort(buffer, ISO7816.OFFSET_CDATA);

        // perform selection if the ID is valid
        if(fileId == FILEID_NDEF_CAPABILITIES || fileId == FILEID_NDEF_DATA) {
            vars[VAR_SELECTED_FILE] = fileId;
        } else {
            ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND);
        }
    }

    /**
     * Process a READ BINARY command
     *
     * This supports simple reads at any offset.
     *
     * The length of the returned data is limited
     * by the maximum R-APDU length as well as by
     * the maximum read size NDEF_MAX_READ.
     *
     * @param apdu to process
     * @throws ISOException on error
     */
    private void processReadBinary(APDU apdu) throws ISOException {
        byte[] buffer = apdu.getBuffer();

        // access the file
        byte[] file = accessFileForRead(vars[VAR_SELECTED_FILE]);

        // get and check the read offset
        short offset = Util.getShort(buffer, ISO7816.OFFSET_P1);
        if(offset < 0 || offset >= file.length) {
            ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
        }

        // determine the output size
        short le = apdu.setOutgoingNoChaining();
        if(le > NDEF_MAX_READ) {
            le = NDEF_MAX_READ;
        }

        // adjust for end of file
        short limit = (short)(offset + le);
        if(limit < 0) {
            ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
        }
        if(limit >= file.length) {
            le = (short)(file.length - offset);
        }

        // send the requested data
        apdu.setOutgoingLength(le);
        apdu.sendBytesLong(file, offset, le);
    }

    /**
     * Access a file for reading
     *
     * This function serves to perform precondition checks
     * before actually operating on a file in a read operation.
     *
     * If this function succeeds then the given fileId was
     * valid, security access has been granted and reading
     * of data for this file is possible.
     *
     * @param fileId of the file to be read
     * @return data array of the file
     * @throws ISOException on error
     */
    private byte[] accessFileForRead(short fileId) throws ISOException {
        byte[] file = null;
        // select relevant data
        if(fileId == FILEID_NDEF_CAPABILITIES) {
            file = capsFile;
        }
        if(fileId == FILEID_NDEF_DATA) {
            file = (byte[])refs[REF_DATA];
        }
        // check that we got anything
        if(file == null) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
        return file;
    }

}