/* * javacard-ndef: JavaCard applet implementing an NDEF tag * Copyright (C) 2015 Ingo Albrecht (prom@berlin.ccc.de) * * 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.aispring.javacard.ndef; import javacard.framework.*; /** * \brief Applet implementing an NDEF type 4 tag * * Implemented to comply with: * NFC Forum * Type 4 Tag Operation Specification * Version 2.0 * * Conformity remarks: * 1. The NDEF data file can be up to 32767 bytes in size, * corresponding to the specification maximum. * 2. No file control information (FCI) is returned in SELECT responses * as allowed by specification requirement RQ_T4T_NDA_034. * 3. Proprietary access modes are being used for custom features, * however they are not exposed in the capability descriptor. * 4. Proprietary files are not being used. * */ public final class NdefApplet extends Applet { /* Instructions */ static final byte INS_SELECT = ISO7816.INS_SELECT; static final byte INS_READ_BINARY = (byte)0xB0; static final byte INS_UPDATE_BINARY = (byte)0xD6; /* File IDs */ static final short FILEID_NONE = (short)0x0000; static final short FILEID_NDEF_CAPABILITIES = (short)0xE103; static final short FILEID_NDEF_DATA = (short)0xE104; /* File access specifications */ static final byte FILE_ACCESS_OPEN = (byte)0x00; static final byte FILE_ACCESS_NONE = (byte)0xFF; static final byte FILE_ACCESS_PROP_CONTACT_ONLY = (byte)0xF0; static final byte FILE_ACCESS_PROP_WRITE_ONCE = (byte)0xF1; /* Parameters for SELECT */ static final byte SELECT_P1_BY_FILEID = (byte)0x00; static final byte SELECT_P2_FIRST_OR_ONLY = (byte)0x0C; /* NDEF mapping version (specification 2.0) */ static final byte NDEF_MAPPING_VERSION = (byte)0x20; /* Application data tags */ static final byte AD_TAG_NDEF_DATA_INITIAL = (byte)0x10; static final byte AD_TAG_NDEF_DATA_ACCESS = (byte)0x11; static final byte AD_TAG_NDEF_DATA_SIZE = (byte)0x12; /* Constants related to capability container */ static final byte CC_LEN_HEADER = 7; static final byte CC_OFF_NDEF_FILE_CONTROL = 0x07; static final byte CC_TAG_NDEF_FILE_CONTROL = 0x04; static final byte CC_LEN_NDEF_FILE_CONTROL = 6; /* Constants related to file control data in capabilities */ static final byte FC_OFF_FILE_ID = 0x00; static final byte FC_OFF_SIZE = 0x02; static final byte FC_OFF_READ_ACCESS = 0x04; static final byte FC_OFF_WRITE_ACCESS = 0x05; /** * Configuration: support for writing * * If disabled then no writing will be allowed after * initialization. Intended for use in combination * with install parameters. */ static final boolean FEATURE_WRITING = true; /** * Configuration: support for install parameters * * If enabled this will allow customization of the applet * during installation by using application parameters. * * Disabling this saves about 200 bytes. */ static final boolean FEATURE_INSTALL_PARAMETERS = true; /** * Configuration: maximum read block size */ static final short NDEF_MAX_READ = 128; /** * Configuration: maximum write block size */ static final short NDEF_MAX_WRITE = 128; /** * Configuration: maximum size of data file * * Two bytes are used for the record length, * the rest will be available for an NDEF record. */ static final short DEFAULT_NDEF_DATA_SIZE = 256; /** * Configuration: read access */ static final byte DEFAULT_NDEF_READ_ACCESS = FILE_ACCESS_OPEN; /** * Configuration: write access */ static final byte DEFAULT_NDEF_WRITE_ACCESS = FILE_ACCESS_OPEN; /** ID of currently selected file */ private short selectedFile; /** NDEF capability file contents */ private byte[] ndefCaps; /** NDEF data file contents */ private byte[] ndefData; /** NDEF data read access policy */ private byte ndefReadAccess; /** NDEF data write access policy */ private byte ndefWriteAccess; /** * 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(buf, offAID, lenAID); } /** * 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) { short dataSize = DEFAULT_NDEF_DATA_SIZE; byte dataReadAccess = DEFAULT_NDEF_READ_ACCESS; byte dataWriteAccess = DEFAULT_NDEF_WRITE_ACCESS; byte[] initBuf = null; short initOff = 0; short initLen = 0; // process application data if(FEATURE_INSTALL_PARAMETERS) { // check TLV consistency if (!UtilTLV.isTLVconsistent(buf, off, len)) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } // DATA INITIAL short initTag = UtilTLV.findTag(buf, off, len, AD_TAG_NDEF_DATA_INITIAL); if (initTag >= 0) { initBuf = buf; initLen = UtilTLV.decodeLengthField(buf, (short) (initTag + 1)); initOff = (short) (initTag + 1 + UtilTLV.getLengthFieldLength(initLen)); // restrict writing, can be overridden dataWriteAccess = FILE_ACCESS_NONE; // adjust size, can be overridden dataSize = (short) (2 + initLen); } // DATA ACCESS short tagAccess = UtilTLV.findTag(buf, off, len, AD_TAG_NDEF_DATA_ACCESS); if (tagAccess >= 0) { short accessLen = UtilTLV.decodeLengthField(buf, (short) (tagAccess + 1)); if (accessLen != 2) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } dataReadAccess = buf[(short) (tagAccess + 2)]; dataWriteAccess = buf[(short) (tagAccess + 3)]; } // DATA SIZE short tagSize = UtilTLV.findTag(buf, off, len, AD_TAG_NDEF_DATA_SIZE); if (tagSize >= 0) { short sizeLen = UtilTLV.decodeLengthField(buf, (short) (tagSize + 1)); if (sizeLen != 2) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } dataSize = Util.getShort(buf, (short) (tagSize + 2)); if (dataSize < 0) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } } } // squash write access if not supported if(!FEATURE_WRITING) { dataWriteAccess = FILE_ACCESS_NONE; } // set up access ndefReadAccess = dataReadAccess; ndefWriteAccess = dataWriteAccess; // create file contents ndefCaps = makeCaps(dataSize, dataReadAccess, dataWriteAccess); ndefData = makeData(dataSize, initBuf, initOff, initLen); } /** * Create and initialize the CAPABILITIES file * * @param dataSize to be allocated * @param dataReadAccess to put in the CC * @param dataWriteAccess to put in the CC * @return an array for use as the CC file */ private byte[] makeCaps(short dataSize, byte dataReadAccess, byte dataWriteAccess) { 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++] = dataReadAccess; // write access caps[pos++] = dataWriteAccess; // check consistency if(pos != capsLen) { ISOException.throwIt(ISO7816.SW_UNKNOWN); } // return the file return caps; } /** * Create and initialize the DATA file * * @param dataSize to be allocated * @param init buffer containing initial data * @param initOff offset of initial data in buffer * @param initLen length of initial data in buffer * @return an array for use as the data file */ private byte[] makeData(short dataSize, byte[] init, short initOff, short initLen) { byte[] data = new byte[dataSize]; // initialize from init, if provided if (FEATURE_INSTALL_PARAMETERS) { if (init != null && initLen > 0) { // container size Util.setShort(data, (short) 0, initLen); // initial data Util.arrayCopyNonAtomic(init, initOff, data, (short) 2, initLen); } } return data; } /** * Fix up a capability container * * This will be called to fix up capabilities before * they are actually sent out to the host device. * * Currently this only fixes up the access policies * so as to hide our proprietary policies. * * @param caps buffer containing CC to fix * @param off offset of CC in buffer * @param len of CC in buffer */ private void fixCaps(byte[] caps, short off, short len) { short offNFC = (short)(off + CC_OFF_NDEF_FILE_CONTROL + 2); short offR = (short)(offNFC + FC_OFF_READ_ACCESS); short offW = (short)(offNFC + FC_OFF_WRITE_ACCESS); caps[offR] = fixAccess(ndefData, ndefReadAccess); caps[offW] = fixAccess(ndefData, ndefWriteAccess); } /** * 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 */ @Override public void process(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); byte ins = buffer[ISO7816.OFFSET_INS]; // handle selection of the applet if(selectingApplet()) { selectedFile = FILEID_NONE; return; } // secure messaging is not supported if(apdu.isSecureMessagingCLA()) { ISOException.throwIt(ISO7816.SW_SECURE_MESSAGING_NOT_SUPPORTED); } // process commands to the applet if(apdu.isISOInterindustryCLA()) { switch (ins) { case INS_SELECT: processSelect(apdu); break; case INS_READ_BINARY: processReadBinary(apdu); break; case INS_UPDATE_BINARY: if(FEATURE_WRITING) { processUpdateBinary(apdu); } else { ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } break; default: 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]; byte lc = buffer[ISO7816.OFFSET_LC]; // we only support what the NDEF spec prescribes if(p1 != SELECT_P1_BY_FILEID) { ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); } if(p2 != SELECT_P2_FIRST_OR_ONLY) { ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); } // 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 switch(fileId) { case FILEID_NDEF_CAPABILITIES: case FILEID_NDEF_DATA: selectedFile = fileId; break; default: 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(selectedFile); // 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 if(selectedFile == FILEID_NDEF_CAPABILITIES) { // send fixed capabilities Util.arrayCopyNonAtomic(file, (short)0, buffer, (short)0, (short)file.length); fixCaps(buffer, (short)0, (short)file.length); apdu.setOutgoingLength(le); apdu.sendBytesLong(buffer, offset, le); } else { // send directly apdu.setOutgoingLength(le); apdu.sendBytesLong(file, offset, le); } } /** * Process an UPDATE BINARY command * * Supports simple writes at any offset. * * The amount of data that can be written in one * operation is limited both by maximum C-APDU * length and the maximum write size NDEF_MAX_WRITE. * * @param apdu to process * @throws ISOException on error */ private void processUpdateBinary(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); // access the file byte[] file = accessFileForWrite(selectedFile); // get and check the write offset short offset = Util.getShort(buffer, ISO7816.OFFSET_P1); if(offset < 0 || offset >= file.length) { ISOException.throwIt(ISO7816.SW_WRONG_P1P2); } // get and check the input size short lc = (short)(buffer[ISO7816.OFFSET_LC] & 0xFF); if(lc > NDEF_MAX_WRITE) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // file limit checks short limit = (short)(offset + lc); if(limit < 0 || limit >= file.length) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } // perform the update Util.arrayCopy(buffer, ISO7816.OFFSET_CDATA, file, offset, lc); } /** * Check if file access should be granted * * This will perform all necessary checks to determine * if an operation can currently be allowed within the * policy specified in ACCESS. * * @param access policy to be checked * @return true if access granted, false otherwise */ private boolean checkAccess(byte[] data, byte access) { // get protocol and media information byte protocol = APDU.getProtocol(); byte media = (byte)(protocol & APDU.PROTOCOL_MEDIA_MASK); // make the decision switch(access) { case FILE_ACCESS_OPEN: return true; case FILE_ACCESS_PROP_CONTACT_ONLY: return media == APDU.PROTOCOL_MEDIA_DEFAULT; case FILE_ACCESS_PROP_WRITE_ONCE: return data[0] == 0 && data[1] == 0; default: case FILE_ACCESS_NONE: return false; } } /** * Fix up an access policy to reflect current state * * This is used to squash our custom access policies * so that we do not have to present a proprietary * policy to unsuspecting host devices. * * @param data of the file for which to fix * @param access policy for to fix * @return a fixed access policy */ private byte fixAccess(byte[] data, byte access) { // figure out the right policy switch(access) { // by default we pass through default: return access; // these two require fixing case FILE_ACCESS_PROP_CONTACT_ONLY: case FILE_ACCESS_PROP_WRITE_ONCE: return (checkAccess(data, access)) ? FILE_ACCESS_OPEN : FILE_ACCESS_NONE; } } /** * 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; byte access = FILE_ACCESS_NONE; // select relevant data if(fileId == FILEID_NDEF_CAPABILITIES) { file = ndefCaps; access = FILE_ACCESS_OPEN; } if(fileId == FILEID_NDEF_DATA) { file = ndefData; access = ndefReadAccess; } // check that we got anything if(file == null) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } // perform access checks if(!checkAccess(file, access)) { ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); } return file; } /** * Access a file for writing * * This function serves to perform precondition checks * before actually operating on a file in a write operation. * * If this function succeeds then the given fileId was * valid, security access has been granted and writing * of data for this file is possible. * * @param fileId of the file to be written * @return data array of the file * @throws ISOException on error */ private byte[] accessFileForWrite(short fileId) throws ISOException { byte[] file = null; byte access = FILE_ACCESS_NONE; // CC can not be written if(fileId == FILEID_NDEF_CAPABILITIES) { ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); } // select relevant data if(fileId == FILEID_NDEF_DATA) { file = ndefData; access = ndefWriteAccess; } // check that we got something if(file == null) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } // perform access checks if(!checkAccess(file, access)) { ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); } return file; } }