diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7f1498f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: java + +env: + - JC_HOME=$TRAVIS_BUILD_DIR/jckit/java_card_kit-2_2_2 + +before_script: + - mkdir jckit + - cd jckit + - "[ -f java_card_kit-2_2_2-linux.zip ] || curl -L http://download.oracle.com/otn-pub/java/java_card_kit/2.2.2/java_card_kit-2_2_2-linux.zip -o java_card_kit-2_2_2-linux.zip --cookie oraclelicense=accept-securebackup-cookie" + - unzip java_card_kit-2_2_2-linux.zip + - cd java_card_kit-2_2_2/ + - unzip java_card_kit-2_2_2-rr-bin-linux-do.zip + - cd ../.. + - mkdir ext + - cd ext + - mkdir ant + - cd ant + - "[ -f ant-javacard.jar ] || curl -L https://github.com/martinpaljak/ant-javacard/releases/download/v1.3/ant-javacard.jar -o ant-javacard.jar" + - cd ../.. + - CLASSPATH=$CLASSPATH:$JC_HOME/lib + +script: ant dist + +cache: + files: + - $TRAVIS_BUILD_DIR/java_card_kit-2_2_2-linux.zip diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..e717d31 --- /dev/null +++ b/build.xml @@ -0,0 +1,19 @@ + + + Builds the project. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/mysmartlogon/gidsApplet/ApplicationFile.java b/src/com/mysmartlogon/gidsApplet/ApplicationFile.java new file mode 100644 index 0000000..b1e0985 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/ApplicationFile.java @@ -0,0 +1,73 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.Util; + +public class ApplicationFile extends DedicatedFile { + + + byte[] fileControlInformation = null; + byte[] fileManagementData = null; + public ApplicationFile(short fileID, byte[] fileControlParameter, byte[] fileControlInformation, byte[] fileManagementData) { + super(fileID, fileControlParameter); + this.fileControlInformation = fileControlInformation; + this.fileManagementData = fileManagementData; + } + + /** + * \brief Check if this is the AID of the application + * + * \param name The array containing the name to compare with the file's name. + * + * \param offset The offset at where the name begins. + * + * \param length The length of the name. + * + * \return false if the DF has no name or the names do not match, + * true else. + */ + public boolean isName(byte[] name, short offset, short length) { + short namePos; + short aidlen = 0; + short i; + // Find the position of the AID tag (4F) in the fci. + try { + namePos = UtilTLV.findTag(fileControlInformation, (short)2, fileControlInformation[(short)1], (byte) 0x4F); + } catch (NotFoundException e) { + // This DF has no name. + return false; + } catch (InvalidArgumentsException e) { + return false; + } + // This ADF has a AID. + try { + aidlen = UtilTLV.decodeLengthField(fileControlInformation, (short)(namePos+1)); + if (aidlen < length) { + // aid len to check is to big to match + return false; + } + } catch (InvalidArgumentsException e) { + return false; + } + // Advance namePos from "tag" to value. + try { + namePos += 1 + UtilTLV.getEncodingLengthFieldLength(length); + } catch(InvalidArgumentsException e) { + return false; + } + // check if the name can be a part of the AID + for (i = 0; i < (short)(aidlen - length +1); i++) { + if ((byte)0 == Util.arrayCompare(name, offset, fileControlInformation, (short)(namePos + i), length) ) { + return true; + } + } + return false; + } + + public byte[] getFileManagementData() { + return fileManagementData; + } + + public byte[] getFileControlInformation() { + return fileControlInformation; + } +} diff --git a/src/com/mysmartlogon/gidsApplet/BerTlvFile.java b/src/com/mysmartlogon/gidsApplet/BerTlvFile.java new file mode 100644 index 0000000..9800da2 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/BerTlvFile.java @@ -0,0 +1,185 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.JCSystem; +import javacard.framework.Util; + +public class BerTlvFile extends ElementaryFile { + + private static final short ELEMENT_COUNT_START = 10; + private static final short ELEMENT_COUNT_MAX = 30; // set to max. 16383 + + private Record[] children; + private byte currentNumChildren; + + /** + * \brief Instantiate a new BER-TLV EF. No data is being added at this point. + * + * \param fileControlInformation The array of bytes containing the valid (!) File Control Information. + * It must contain the File ID (Tag 83). No Copy is made. + * + * \param maxRecords The maximum amount of saved records. + * + * \attention No copy of the FCI is made. Do not pass any buffer that is altered + * later (e.g. the apdu buffer). Max length 257 bytes as the length + * of the FCI Tag (6F) must be a byte. + * + * \attention To be safe, use IsoFileSystem.getSafeFile() to instantiate files. + * + * \throw IllegalArgumentException If necessary tags in the FCI are missing. + */ + public BerTlvFile(short fileID, byte[] fileControlInformation) { + super(fileID, fileControlInformation); + this.children = new Record[ELEMENT_COUNT_START]; + this.currentNumChildren = 0; + } + + @Override + void clearContents() { + short i; + + for(i = 0; i < currentNumChildren; i++) { + children[i].clearContents(); + children[i] = null; + } + + } + + /** + * \brief Delete a DO + * + * This method requests garbage collection. + * + * \param childNum internal index + */ + protected void deleteChildren(short childNum) { + + children[childNum] = null; + currentNumChildren--; // We have one less children now. + + // Fill up empty field in children array. + // The last children is one ahead, so it is at currentNumChildren. + if(childNum < currentNumChildren) { + children[childNum] = children[currentNumChildren]; + } + + // Clean up the old file object. + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } + + /** + * \brief remove a DO + * + * \param children The children to add. + * + * \throw NotEnoughSpaceException If CHILDREN_COUNT_MAX is reached. + * @param size + * @param offset_cdata + */ + public Record addChildren(byte[] buffer, short offset, short wholelength, short lengthavailable) throws NotEnoughSpaceException { + // try to find a previous TLV + short i; + short lengthToCopy = (lengthavailable > wholelength ? wholelength: lengthavailable); + for(i = 0; i < currentNumChildren; i++) { + byte[] value = children[i].GetData(); + + if (UtilTLV.IsBERTLVTagEqual(buffer, offset, (short) (offset + lengthavailable), value)) { + // found => replace or erase ? + + // erase if empty DO pushed and already empty DO stored + short oldlen = UtilTLV.GetBERTLVDataLen(value, (short) 0, (short) value.length); + short newlen = UtilTLV.GetBERTLVDataLen(buffer, offset, (short) (offset + lengthavailable)); + if (oldlen == 0) { + if (newlen == 0) { + deleteChildren(i); + return null; + } + } + // replace + if (oldlen == newlen) { + // no need to add / remove data, just replace the buffer + Util.arrayCopyNonAtomic(buffer, offset, value, (short) 0, lengthToCopy); + } else { + // remove previous data, add new + byte[] data = new byte[wholelength]; + Util.arrayCopyNonAtomic(buffer, offset, data, (short) 0, lengthToCopy); + children[i] = null; + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + children[i] = new Record(data); + } + return children[i]; + } + + } + + + // First we have to check for enough space. + if(currentNumChildren >= (short)children.length) { + Record[] newChildren = null; + // The array is full - we try to increase the size. + if((short)(children.length * 2) <= ELEMENT_COUNT_MAX) { + // Doubling the size is possible. + newChildren = new Record[(short)(children.length * 2)]; + copyFileArrayRefs(children, newChildren); + } else { + // Doubling not possible - try to at least increase to CHILDREN_COUNT_MAX. + if(currentNumChildren < ELEMENT_COUNT_MAX) { + newChildren = new Record[ELEMENT_COUNT_MAX]; + copyFileArrayRefs(children, newChildren); + } else { + // CHILDREN_COUNT_MAX exceeded. No "space" left. Fail. + throw NotEnoughSpaceException.getInstance(); + } + } + children = newChildren; // Initial children array is now garbage. + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } // We have enough space (now). + byte[] data = new byte[wholelength]; + Util.arrayCopyNonAtomic(buffer, offset, data, (short) 0, lengthToCopy); + children[currentNumChildren++] = new Record(data); + return children[(short) (currentNumChildren-1)]; + } + + /** + * \brief Copies the references from one File array to the other. + * + * \attention Although only references are copied, this is probably still quite expensive because + * writing to the EEPROM is. Only use this for operations that are not called often (Creating and deleting files etc.). + * + * \param src The source File array to copy from. + * + * \param dest The destination File array to copy to. It MUST be at least of size of the src array. + */ + private static void copyFileArrayRefs(Record[] src, Record[] dest) { + short i = 0; + short length = src.length > dest.length ? (short)dest.length : (short)src.length; + + for(i=0; i < length; i++) { + dest[i] = src[i]; + } + return; + } + + public Record getData(byte[] tag, short offset, short len) throws NotFoundException { + short i; + + for(i = 0; i < currentNumChildren; i++) { + byte[] value = children[i].GetData(); + if(UtilTLV.IsBERTLVTagEqual(tag, offset, len, value)) { + return children[i]; + } + } + + throw NotFoundException.getInstance(); + } + + public Record[] getAllData() { + return children; + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/CRTKeyFile.java b/src/com/mysmartlogon/gidsApplet/CRTKeyFile.java new file mode 100644 index 0000000..423d1a6 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/CRTKeyFile.java @@ -0,0 +1,327 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.JCSystem; +import javacard.framework.Util; +import javacard.security.CryptoException; +import javacard.security.KeyBuilder; +import javacard.security.KeyPair; +import javacard.security.RSAPrivateCrtKey; +import javacard.security.RSAPublicKey; + +public class CRTKeyFile extends ElementaryFile { + + private final short posCRT; + private final short lenCRT; + //private final byte keyId; + + private KeyPair keyPair = null; + private byte[] symetricKey = null; + + public CRTKeyFile(short fileID, byte[] fileControlInformation, short pos, short len) { + super(fileID, fileControlInformation); + posCRT = pos; + lenCRT = len; + //keyId = (byte) (fileID & 0xFF); + } + + public static void CheckCRT(byte[] fcp, short pos, short len) throws InvalidArgumentsException { + if (len < 11 || len > 127) { + throw InvalidArgumentsException.getInstance(); + } + /*short innerOffset = (short) (pos + 2); + short innerLength = (short) (len - 2); + // Search the CRT tag (A5). If not found, then raise an error + try { + pos = UtilTLV.findTag(fcp, innerOffset, innerLength, (byte) 0xA4); + len = UtilTLV.decodeLengthField(fcp, (short)(pos+1)); + } catch (NotFoundException e) { + throw InvalidArgumentsException.getInstance(); + }*/ + } + + @Override + void clearContents() { + if (keyPair != null) { + keyPair.getPrivate().clearKey(); + } + } + + private void ClearKeys() { + if (symetricKey != null) { + symetricKey = null; + } + if (keyPair != null) { + keyPair.getPrivate().clearKey(); + } + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } + + public void SaveKey(KeyPair kp) { + ClearKeys(); + keyPair = kp; + } + + public KeyPair GetKey() { + return keyPair; + } + + public void CheckUsage(byte operation, byte algRef) throws NotFoundException { + short innerPos = (short) (posCRT+2), pos = 0; + short innerLen = 0, len = 0; + boolean found = false; + while( !found) { + // Search the operation tag. If not found, then raise an error + try { + innerPos = UtilTLV.findTag(fcp, innerPos, lenCRT, operation); + innerLen = UtilTLV.decodeLengthField(fcp, (short)(innerPos+1)); + } catch (NotFoundException e) { + throw NotFoundException.getInstance(); + } catch (InvalidArgumentsException e) { + throw NotFoundException.getInstance(); + } + try { + pos = UtilTLV.findTag(fcp, (short) (innerPos+2), innerLen, (byte) 0x80); + len = UtilTLV.decodeLengthField(fcp, (short)(pos+1)); + if (len != 1) { + throw InvalidArgumentsException.getInstance(); + } + byte ref = fcp[(short) (pos+2)]; + if (algRef == ref) { + found = true; + break; + } + } catch (NotFoundException e) { + // search next tag + continue; + } catch (InvalidArgumentsException e) { + throw NotFoundException.getInstance(); + } + innerPos += 2; + innerPos += innerLen; + } + if (!found) { + throw NotFoundException.getInstance(); + } + } + + public void importKey(byte[] buffer, short offset, short length) throws InvalidArgumentsException { + // focused on A5 tag + short innerPos = 0, innerLen = 0; + short pos = 0, len = 0; + // keytype is missing for symetric key + byte keytype = 1; + byte keyref = 0; + if (buffer[offset] != (byte) 0xA5) { + throw InvalidArgumentsException.getInstance(); + } + + innerPos = (short) (offset + 1 + UtilTLV.getLengthFieldLength(buffer, (short) (offset+1))); + innerLen = UtilTLV.decodeLengthField(buffer, (short) (offset+1)); + + try { + pos = UtilTLV.findTag(buffer, innerPos, innerLen, (byte) 0x83); + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + if (len != 1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + keytype = buffer[(short) (pos+2)]; + } catch (NotFoundException e) { + // optional tag: default = symetric key + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try { + pos = UtilTLV.findTag(buffer, innerPos, innerLen, (byte) 0x84); + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + if (len != 1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + // key ref used to encrypt the imported key + keyref = buffer[(short) (pos+2)]; + if (keyref != 0) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + } catch (NotFoundException e) { + // optional tag: default = none + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + try { + pos = UtilTLV.findTag(buffer, innerPos, innerLen, (byte) 0x87); + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if (keytype == 1) { + importSymetricKey(buffer, pos, len); + } else if (keytype == 2) { + importRsaKey(buffer, pos, len); + } else { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + } + + private void importRsaKey(byte[] buffer, short offset, short length) throws InvalidArgumentsException { + + short pos = offset; + short len = 0; + RSAPrivateCrtKey rsaPrKey = null; + RSAPublicKey rsaPuKey = null; + if (buffer[pos] != 0x30) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if (len > (short) (length +2)) { + throw InvalidArgumentsException.getInstance(); + } + // version; len=1 ; value = 0 + if (buffer[pos++] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + if (buffer[pos++] != 0x01) { + throw InvalidArgumentsException.getInstance(); + } + if (buffer[pos++] != 0x00) { + throw InvalidArgumentsException.getInstance(); + } + // modulus + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + short keysize = (short) (len * 8); + try { + rsaPrKey = (RSAPrivateCrtKey) KeyBuilder.buildKey(KeyBuilder.TYPE_RSA_CRT_PRIVATE, keysize, false); + rsaPuKey = (RSAPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_RSA_PUBLIC, keysize, false); + } catch(CryptoException e) { + if(e.getReason() == CryptoException.NO_SUCH_ALGORITHM) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + ISOException.throwIt(ISO7816.SW_UNKNOWN); + } + rsaPuKey.setModulus(buffer, pos, len); + pos += len; + // public exponent + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + + rsaPuKey.setExponent(buffer, pos, len); + pos += len; + // private exponent + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + pos += len; + // P + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + // the minidriver may prepend a 00 before (len = len+1) and the javacard don't like it => remove the 00 + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + try { + rsaPrKey.setP(buffer, pos, len); + } catch(CryptoException e) { + if(e.getReason() == CryptoException.ILLEGAL_VALUE) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + ISOException.throwIt(ISO7816.SW_UNKNOWN); + } + pos += len; + // Q + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + rsaPrKey.setQ(buffer, pos, len); + pos += len; + // d mod p-1 + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + rsaPrKey.setDP1(buffer, pos, len); + pos += len; + // d mod q-1 + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + rsaPrKey.setDQ1(buffer, pos, len); + pos += len; + // q-1 mod p + if (buffer[pos] != 0x02) { + throw InvalidArgumentsException.getInstance(); + } + len = UtilTLV.decodeLengthField(buffer, (short)(pos+1)); + pos += 1 + UtilTLV.getLengthFieldLength(buffer, (short)(pos+1)); + if ((len & 0x0F) == (byte) 1 && buffer[pos] == 0) { + len -= 1; + pos++; + } + rsaPrKey.setPQ(buffer, pos, len); + pos += len; + + if(rsaPrKey.isInitialized()) { + // If the key is usable, it MUST NOT remain in buf. + Util.arrayFillNonAtomic(buffer, offset, length, (byte)0x00); + ClearKeys(); + this.keyPair = new KeyPair(rsaPuKey, rsaPrKey); + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + + } else { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + } + + private void importSymetricKey(byte[] buffer, short offset, short length) { + ClearKeys(); + byte[] key = new byte[length]; + Util.arrayCopyNonAtomic(buffer, offset, key, (short) 0, length); + this.symetricKey = key; + } + + public byte[] GetSymectricKey() { + return symetricKey; + } +} diff --git a/src/com/mysmartlogon/gidsApplet/DedicatedFile.java b/src/com/mysmartlogon/gidsApplet/DedicatedFile.java new file mode 100644 index 0000000..530238d --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/DedicatedFile.java @@ -0,0 +1,375 @@ +/* + * IsoApplet: A Java Card PKI applet aiming for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.*; + +/** + * \brief The DedicatedFile class. + * + * A DedicatedFile object acts as a container for objects of any File subclass except the IsoFileSystem class itself. + * It emulates the ISO "Dedicated File". + * Children are stored as references. This means that if a File is being altered after it was added as child, + * the child is effectively altered as well because it is the same object. + * + * The initial size of the array storing the references to the children can be set before compilation. + * This class tries to increase the size until a maximum value. If you do not want that kind of behavior, + * set CHILDREN_COUNT_MAX to the same value as CHILDREN_COUNT_START. + */ +public class DedicatedFile extends File { + private static final short CHILDREN_COUNT_START = 10; + private static final short CHILDREN_COUNT_MAX = 30; // set to max. 16383 + + public static final byte SPECIFY_EF = 0x01; + public static final byte SPECIFY_DF = 0x02; + public static final byte SPECIFY_ANY = 0x03; + + private byte currentNumChildren; + private File[] children; + + + /** + * \brief Instantiate a new DedicatedFile. + * + * \param fileID The file ID. Should be unique inside the filesystem. + * + * \param fileControlInformation The array of bytes containing the valid (!) File Control Information. + * It must contain the File ID (Tag 83). No Copy is made. + * + * \attention No copy of the fcp is made. Do not pass any buffer that is altered + * later (e.g. the apdu buffer). Max length 257 bytes as the length + * of the fcp Tag (6F) must be a byte. + * + * \attention To be safe, use IsoFilesystem.getSafeFile() to instantiate files. + * + * \return The DedicatedFile. + */ + public DedicatedFile(short fileID, byte[] fileControlInformation) { + super(fileID, fileControlInformation); + this.currentNumChildren = 0; + this.children = new File[CHILDREN_COUNT_START]; + } + + /** + * \brief Clear the contents of the file. + * + * When deleting a DedicatedFile, all children will be lost as well. + * Their content should be cleared as well. + */ + @Override + void clearContents() { + short i; + + for(i = 0; i < currentNumChildren; i++) { + children[i].clearContents(); + children[i] = null; + } + } + + /** + * \brief Check if this is the name of this DedicatedFile. + * + * \param name The array containing the name to compare with the file's name. + * + * \param offset The offset at where the name begins. + * + * \param length The length of the name. + * + * \return false if the DF has no name or the names do not match, + * true else. + */ + public boolean isName(byte[] name, short offset, short length) { + short namePos; + // Find the position of the DF name tag (84) in the fcp. + try { + namePos = UtilTLV.findTag(fcp, (short)2, fcp[(short)1], (byte) 0x84); + } catch (NotFoundException e) { + // This DF has no name. + return false; + } catch (InvalidArgumentsException e) { + return false; + } + // This DF has a name. + try { + if(length != UtilTLV.decodeLengthField(fcp, (short)(namePos+1))) { + // The names do not have equal length. + return false; + } + } catch (InvalidArgumentsException e) { + return false; + } + // Advance namePos from "tag" to value. + try { + namePos += 1 + UtilTLV.getEncodingLengthFieldLength(length); + } catch(InvalidArgumentsException e) { + return false; + } + return ( (byte)0 == Util.arrayCompare(name, offset, fcp, namePos, length) ); + } + + /** + * \brief Get the amount of children under this Dedicated File. + * + * \return The amount of children under this DF. + */ + public byte getChildrenCount() { + return this.currentNumChildren; + } + + /** + * \brief Get a children of this Dedicated File. + * + * This method returns the specified children of this DF. Can be used in conjunction with getChildrenCount() + * to iterate over all children. + * + * \param num The number of the children, starting at 0 to getChildrenCount(). The references can change when + * children had been deleted. + * + * \throw NotFoundException If the specified file was not found unter this DF. + * + * \return The children file if present. May be a DedicatedFile or any non-abstract ElementaryFile subclass. + */ + public File getChildren(byte num) throws NotFoundException { + if(num >= this.currentNumChildren) { + throw NotFoundException.getInstance(); + } + return children[num]; + } + + /** + * \brief Delete a direct children of this DF. + * + * This method requests garbage collection. + * + * \param fileID The file ID of the children to delete. + * + * \throw NotFoundException It no children has the given fileID. + */ + public void deleteChildren(short fileID) throws NotFoundException { + short childNum = -1; + short i; + + for(i = 0; i < currentNumChildren; i++) { + if(fileID == children[i].getFileID()) { + childNum = i; + break; + } + } + + if(childNum == -1) { + throw NotFoundException.getInstance(); + } + + if( ! JCSystem.isObjectDeletionSupported()) { + // Old file will stay as garbage in the EEPROM - at least clear the contents. + children[childNum].clearContents(); + } + + children[childNum] = null; + currentNumChildren--; // We have one less children now. + + // Fill up empty field in children array. + // The last children is one ahead, so it is at currentNumChildren. + if(childNum < currentNumChildren) { + children[childNum] = children[currentNumChildren]; + } + + // Clean up the old file object. + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } + + /** + * \brief Add a children to this DF. + * + * \param children The children to add. May be a DedicatedFile or any non-abstract ElemetaryFile subclass. + * + * \throw NotEnoughSpaceException If CHILDREN_COUNT_MAX is reached. + */ + public void addChildren(File childFile) throws NotEnoughSpaceException { + // First we have to check for enough space. + if(currentNumChildren >= (short)children.length) { + File[] newChildren = null; + // The array is full - we try to increase the size. + if((short)(children.length * 2) <= CHILDREN_COUNT_MAX) { + // Doubling the size is possible. + newChildren = new File[(short)(children.length * 2)]; + copyFileArrayRefs(children, newChildren); + } else { + // Doubling not possible - try to at least increase to CHILDREN_COUNT_MAX. + if(currentNumChildren < CHILDREN_COUNT_MAX) { + newChildren = new File[CHILDREN_COUNT_MAX]; + copyFileArrayRefs(children, newChildren); + } else { + // CHILDREN_COUNT_MAX exceeded. No "space" left. Fail. + throw NotEnoughSpaceException.getInstance(); + } + } + children = newChildren; // Initial children array is now garbage. + if(JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } // We have enough space (now). + children[currentNumChildren++] = childFile; + return; + } + + /** + * \brief Copies the references from one File array to the other. + * + * \attention Although only references are copied, this is probably still quite expensive because + * writing to the EEPROM is. Only use this for operations that are not called often (Creating and deleting files etc.). + * + * \param src The source File array to copy from. + * + * \param dest The destination File array to copy to. It MUST be at least of size of the src array. + */ + private static void copyFileArrayRefs(File[] src, File[] dest) { + short i = 0; + short length = src.length > dest.length ? (short)dest.length : (short)src.length; + + for(i=0; i < length; i++) { + dest[i] = src[i]; + } + return; + } + + + /** + * \brief Recursively search the children of this file using the DedicatedFile name. + * + * \param name The DF name of at most 16 bytes according to ISO. + * + * \param nameOffset The position in the name array at which the name beigns. + * + * \param nameLength The length of the name + * + * \throw NotFoundException If the specified file was not found among all (sub-)children of this file. + * + * \return A reference to the DedicatedFile if found. + */ + public DedicatedFile findDedicatedFileByNameRec(byte[] name, short nameOffset, short nameLength) throws NotFoundException { + short i; + for(i=0; i < currentNumChildren; i++) { + if(children[i] instanceof DedicatedFile) { + if(((DedicatedFile)children[i]).isName(name, nameOffset, nameLength)) { + return (DedicatedFile) children[i]; + } + try { + return ((DedicatedFile)children[i]).findDedicatedFileByNameRec(name, nameOffset, nameLength); + } catch(NotFoundException e) { + // Ignore this exception until the last children has unsuccessfully been visited. + } + } + } + throw NotFoundException.getInstance(); + } + + /** + * \brief Recursively search the children of this file using the file ID. + * + * \param fileID The file ID of the file to search for. + * + * \throw NotFoundException If the specified file was not found among all (sub-)children of this file. + * + * \return A reference to the File if found. + */ + public File findChildrenRec(short fileID, byte flag) throws NotFoundException { + short i; + for(i=0; i < currentNumChildren; i++) { + if(children[i].getFileID() == fileID) { + if((flag == SPECIFY_ANY) + || (flag == SPECIFY_DF && children[i] instanceof DedicatedFile) + || (flag == SPECIFY_EF && children[i] instanceof ElementaryFile)) { + return children[i]; + } else { + // File with specified FID and requested file type do not match. + throw NotFoundException.getInstance(); + } + } + if(children[i] instanceof DedicatedFile) { + try { + return ((DedicatedFile)children[i]).findChildrenRec(fileID, flag); + } catch(NotFoundException e) { + // Ignore this exception until the last children has unsuccessfully been visited. + } + } + } + throw NotFoundException.getInstance(); + } + + /** + * \brief Find the children using the specified path. + * + * \param path The byte array containing the path. This is a concatenation of File IDs. A fileID is 2 bytes long. + * + * \param pathOffset The position at which the path begins. It starts with children of this file. + * + * \param pathLength The length of the path in bytes. Two times the FIDs contained. + * + * \throw NotFoundException If the path does not lead to a file. + * + * \return The File if found. + */ + public File findChildrenByPath(byte[] path, short pathOffset, short pathLength) throws NotFoundException { + byte childPos; + short pathPos; + short nextFileID; + + DedicatedFile df = this; + for(pathPos=pathOffset; pathPos < (short) ((pathLength+pathOffset)-1); pathPos+=2) { + nextFileID = Util.getShort(path, pathPos); + for(childPos=0; childPos < df.getChildrenCount(); childPos++) { + if(nextFileID == df.getChildren( childPos ).getFileID()) { + // We found the next node in the path. + if(pathPos == (short) (pathOffset+pathLength-2)) { + // It is the last File in the path with a matching FID. + // We are done. + return df.getChildren(childPos); + } else if(df.getChildren( childPos ) instanceof DedicatedFile) { + // We still have to search for children. + // Luckily, the last file we found is a DF. :-) + df = (DedicatedFile) df.getChildren( childPos); + break; + } else { + // Matching file ID, has children according to path, but is no DF. + // Something really bad happened or the path was invalid! + throw NotFoundException.getInstance(); + } + } + } + } + // We could not find the file with that path. + throw NotFoundException.getInstance(); + } + +} + + + + + + + + + + + diff --git a/src/com/mysmartlogon/gidsApplet/ElementaryFile.java b/src/com/mysmartlogon/gidsApplet/ElementaryFile.java new file mode 100644 index 0000000..058f7c4 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/ElementaryFile.java @@ -0,0 +1,59 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +/** + * \brief The abstract class ElementaryFile. + * + * It's main purpose is to be able to easily differentiate between EFs and DFs. + */ +public abstract class ElementaryFile extends File { + private byte shortFileID; + + /** + * \brief Abstract constructor to be called by subclasses. + * + * \param fileControlInformation The array of bytes containing the valid (!) File Control Information. + * No Copy is made. + * + * \param fileID The ID of the file. Consistency with tag 0x83 from the FCI is NOT checked. + * + * \attention No copy of the FCI is made. Do not pass any buffer that is altered + * later (e.g. the apdu buffer). Max length 257 bytes as the length + * of the FCI Tag (6F) must be a byte. + * + * \attention To be safe, use IsoFileSystem.getSafeFile() to instantiate files. + */ + public ElementaryFile(short fileID, byte[] fileControlInformation) { + super(fileID, fileControlInformation); + // If not specified otherwise, the SFI should be the last 5 bits of the FID. + this.shortFileID = (byte) (fileID & 0x001F); + } + + /** + * \brief Get the short file Identifier (SFI). + * + * \return The SFI. + */ + public byte getShortFileID() { + return shortFileID; + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/ErrorCode.java b/src/com/mysmartlogon/gidsApplet/ErrorCode.java new file mode 100644 index 0000000..a079efc --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/ErrorCode.java @@ -0,0 +1,9 @@ +package com.mysmartlogon.gidsApplet; + +public class ErrorCode { + public static final short SW_PIN_TRIES_REMAINING = 0x63C0; // See ISO 7816-4 section 7.5.1 + public static final short SW_REFERENCE_DATA_NOT_FOUND = 0x6A88; + public static final short SW_COMMAND_NOT_ALLOWED_GENERAL = 0x6900; + public static final short SW_TERMINATION_STATE = 0x6285; + public static final short SW_COMMAND_INCOMPATIBLE_WITH_FILE_STRUCTURE = 0x6981; +} diff --git a/src/com/mysmartlogon/gidsApplet/File.java b/src/com/mysmartlogon/gidsApplet/File.java new file mode 100644 index 0000000..9d5b537 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/File.java @@ -0,0 +1,263 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.ISO7816; +import javacard.framework.ISOException; + +/** + * \brief The File class acting as superclass for any file. + */ +public abstract class File { + private final short fileID; + private DedicatedFile parentDF; + + final byte[] fcp; + private final short aclPos; + private byte state; + /* Access Control Operations */ + public static final byte ACL_OP_01 = (byte) 0x01; + public static final byte ACL_OP_02 = (byte) 0x02; + public static final byte ACL_OP_04 = (byte) 0x04; + public static final byte ACL_OP_08 = (byte) 0x08; + public static final byte ACL_OP_10 = (byte) 0x10; + public static final byte ACL_OP_20 = (byte) 0x20; + public static final byte ACL_OP_40 = (byte) 0x40; + + public static final byte ACL_OP_DF_DELETE_CHILD = (byte) 0x01; + public static final byte ACL_OP_DF_CREATE_EF = (byte) 0x02; + public static final byte ACL_OP_DF_CREATE_DF = (byte) 0x04; + public static final byte ACL_OP_DF_DEACTIVATE = (byte) 0x08; + public static final byte ACL_OP_DF_ACTIVATE = (byte) 0x10; + public static final byte ACL_OP_DF_TERMINATE = (byte) 0x20; + public static final byte ACL_OP_DF_DELETE_SELF = (byte) 0x40; + + public static final byte ACL_OP_EF_READ = (byte) 0x01; + public static final byte ACL_OP_EF_UPDATE = (byte) 0x02; + public static final byte ACL_OP_EF_WRITE = (byte) 0x04; + public static final byte ACL_OP_EF_DEACTIVATE = (byte) 0x08; + public static final byte ACL_OP_EF_ACTIVATE = (byte) 0x10; + public static final byte ACL_OP_EF_TERMINATE = (byte) 0x20; + public static final byte ACL_OP_EF_DELETE = (byte) 0x40; + + public static final byte ACL_OP_DO_GET_DATA = (byte) 0x01; + public static final byte ACL_OP_DO_PUT_DATA = (byte) 0x02; + + public static final byte ACL_OP_KEY_GETPUBLICKEY = (byte) 0x01; + public static final byte ACL_OP_KEY_PUTKEY = (byte) 0x02; + public static final byte ACL_OP_KEY_MANAGE_SEC_ENV = (byte) 0x04; + public static final byte ACL_OP_KEY_GENERATE_ASYMETRIC = (byte) 0x08; + + + /* Card/Applet lifecycle states */ + // see 7.4.10 Life cycle status table 14 + public static final byte STATE_CREATION = (byte) 0x01; // No restrictions, PUK not set yet. + public static final byte STATE_INITIALISATION = (byte) 0x03; // PUK set, PIN not set yet. PUK may not be changed. + public static final byte STATE_OPERATIONAL_ACTIVATED = (byte) 0x07; // PIN is set, data is secured. + public static final byte STATE_OPERATIONAL_DEACTIVATED = (byte) 0x06; // Applet usage is deactivated. (Unused at the moment.) + public static final byte STATE_TERMINATED = (byte) 0x0F; // Applet usage is terminated. (Unused at the moment.) + + + /** + * \brief Abstract constructor to be called by subclasses. + * + * \param fileID The ID of the file. + * + * \param fileControlInformation The FCI according to ISO 7816-4 table 12. Necessary tags: 82, 83. No copy is made. + */ + public File(short fileID, byte[] fileControlParameter) { + this.fileID = fileID; + this.parentDF = null; + this.fcp = fileControlParameter; + // Save the position of the ACL (Value field) in the FCI for performance reasons. + // If the position is -1, then every action may be performed. + + // try the following tag by order + // tag 0x86 = security attribute in proprietary format + // tag 0x8C = compact format + + short pos; + try { + pos = UtilTLV.findTag(fcp, (short) 2, fcp[(short)1], (byte) 0x8C); + + } catch (NotFoundException e) { + pos = -1; + } catch (InvalidArgumentsException e) { + pos = -1; + } + this.aclPos = pos; + + state = STATE_CREATION; + } + + public void CheckPermission(GidsPINManager pinManager, byte flag_operation) { + if (state == STATE_CREATION) { + if (this instanceof ApplicationFile) { + // every operation is allowed on the application on the creation state + return; + } + if (this instanceof ElementaryFile) { + // only a transition to operational state is allowed + if (flag_operation != ACL_OP_EF_ACTIVATE) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + } + if (this instanceof DedicatedFile) { + // only a transition to operational state is allowed + if (flag_operation != ACL_OP_DF_ACTIVATE) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + } + } else if (state == STATE_TERMINATED) { + if (this instanceof ApplicationFile) { + // every operation is denied on the application on the termination state + ISOException.throwIt(ErrorCode.SW_TERMINATION_STATE); + } + if (this instanceof ElementaryFile) { + // only a transition to operational state is allowed + if (flag_operation != ACL_OP_EF_DELETE) { + ISOException.throwIt(ErrorCode.SW_TERMINATION_STATE); + } + } + if (this instanceof DedicatedFile) { + // only a transition to operational state is allowed + if (flag_operation != ACL_OP_DF_DELETE_SELF) { + ISOException.throwIt(ErrorCode.SW_TERMINATION_STATE); + } + } + } + CheckACLRequirements(pinManager, flag_operation); + } + + + /** + * \brief Get the relevant ACL byte for the operation. + * + * \param flag_operation The operation. One of ACL_OP_*. + * + * \return The ACL byte. + */ + private void CheckACLRequirements(GidsPINManager pinManager, byte flag_operation) { + if(aclPos == -1) { + return; // Any operation is allowed if there is no ACL. + } + byte accessmod = fcp[(short)(aclPos+2)]; + short index = (short)(aclPos+2); + if ((accessmod & ACL_OP_40) != 0) { + index++; + if (flag_operation == ACL_OP_40) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_20) != 0) { + index++; + if (flag_operation == ACL_OP_20) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_10) != 0) { + index++; + if (flag_operation == ACL_OP_10) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_08) != 0) { + index++; + if (flag_operation == ACL_OP_08) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_04) != 0) { + index++; + if (flag_operation == ACL_OP_04) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_02) != 0) { + index++; + if (flag_operation == ACL_OP_02) { + pinManager.CheckACL(fcp[index]); + } + } + if ((accessmod & ACL_OP_01) != 0) { + index++; + if (flag_operation == ACL_OP_01) { + pinManager.CheckACL(fcp[index]); + } + } + // TODO: check if a second ACL is following + // typically ACL for contact & contactless operations + return; // Any operation is allowed if there is no ACL. + } + + + + /** + * \brief Get the file identifier. + * + * \return The file ID. + */ + public short getFileID() { + return this.fileID; + } + + /** + * \brief Get the parent Dedicated File (DF). + * + * \return The parent DF or null if the file had not been added yet. + */ + public DedicatedFile getParentDF() { + return this.parentDF; + } + + /** + * \brief Set the parent Dedicated File (DF). + * + * \param parent the parent DF. + */ + public void setParentDF(DedicatedFile parent) { + this.parentDF = parent; + } + + /** + * \brief Get the File Control Information (FCI). + * + * \return The FCI array. + */ + public final byte[] getFileControlParameter() { + return this.fcp; + } + + public final byte getState() { + return state; + } + + public final void setState(byte state) { + this.state = state; + } + + /** + * \brief Clear the contents of the file. + * + * Used when deleting files and JCSystem.requestObjectDeletion() is not + * implemented. + */ + abstract void clearContents(); +} diff --git a/src/com/mysmartlogon/gidsApplet/GidsApplet.java b/src/com/mysmartlogon/gidsApplet/GidsApplet.java new file mode 100644 index 0000000..79592cb --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/GidsApplet.java @@ -0,0 +1,863 @@ +/* + * GidsApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.Applet; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.APDU; +import javacard.framework.JCSystem; +import javacard.framework.Util; +import javacard.security.KeyBuilder; +import javacard.security.KeyPair; +import javacard.security.PrivateKey; +import javacard.security.PublicKey; +import javacard.security.RSAPublicKey; +import javacardx.crypto.Cipher; +import javacard.security.CryptoException; + +/** + * \brief The GidsApplet class. + * + * This applet has a filesystem and accepts relevant ISO 7816 instructions. + * Access control is forced through a PIN and a PUK. The PUK is optional + * (Set PUK_MUST_BE_SET). Security Operations are being processed directly in + * this class. Only private keys are stored as Key-objects. Only security + * operations with private keys can be performed (decrypt with RSA, sign with RSA, + * sign with ECDSA). + * + * \author Philip Wendland + */ +public class GidsApplet extends Applet { + /* API Version */ + public static final byte API_VERSION_MAJOR = (byte) 0x00; + public static final byte API_VERSION_MINOR = (byte) 0x06; + + /* Card-specific configuration */ + public static final boolean DEF_PRIVATE_KEY_IMPORT_ALLOWED = true; + + /* ISO constants not in the "ISO7816" interface */ + // File system related INS: + public static final byte INS_CREATE_FILE = (byte) 0xE0; + public static final byte INS_UPDATE_BINARY = (byte) 0xD6; + public static final byte INS_READ_BINARY = (byte) 0xB0; + public static final byte INS_DELETE_FILE = (byte) 0xE4; + // Other INS: + public static final byte INS_VERIFY = (byte) 0x20; + public static final byte INS_CHANGE_REFERENCE_DATA = (byte) 0x24; + public static final byte INS_GENERATE_ASYMMETRIC_KEYPAIR = (byte) 0x47; + public static final byte INS_RESET_RETRY_COUNTER = (byte) 0x2C; + public static final byte INS_MANAGE_SECURITY_ENVIRONMENT = (byte) 0x22; + public static final byte INS_PERFORM_SECURITY_OPERATION = (byte) 0x2A; + public static final byte INS_GET_RESPONSE = (byte) 0xC0; + public static final byte INS_PUT_DATA = (byte) 0xDB; + public static final byte INS_GET_CHALLENGE = (byte) 0x84; + public static final byte INS_GENERAL_AUTHENTICATE = (byte) 0x87; + public static final byte INS_GET_DATA = (byte) 0xCB; + public static final byte INS_ACTIVATE_FILE = (byte) 0x44; + public static final byte INS_TERMINATE_DF = (byte) 0xE6; + + private GidsPINManager pinManager = null; + + + /* Member variables: */ + private GidsFileSystem fs = null; + private byte[] currentAlgorithmRef; + private Object[] currentKey; + private TransmitManager transmitManager = null; + private Cipher rsaPkcs1Cipher = null; + private Cipher rsaOaepCipher = null; + private Cipher rsaRawCipher = null; + + + /** + * \brief Installs this applet. + * + * \param bArray + * the array containing installation parameters + * \param bOffset + * the starting offset in bArray + * \param bLength + * the length in bytes of the parameter data in bArray + */ + public static void install(byte[] bArray, short bOffset, byte bLength) { + new GidsApplet(); + } + + /** + * \brief Only this class's install method should create the applet object. + */ + protected GidsApplet() { + + // by default the pin manager is in "initialization mode" + pinManager = new GidsPINManager(); + + transmitManager = new TransmitManager(); + + currentAlgorithmRef = JCSystem.makeTransientByteArray((short)1, JCSystem.CLEAR_ON_DESELECT); + currentKey = JCSystem.makeTransientObjectArray((short)1, JCSystem.CLEAR_ON_DESELECT); + + rsaPkcs1Cipher = Cipher.getInstance(Cipher.ALG_RSA_PKCS1, false); + try { + rsaOaepCipher = Cipher.getInstance(Cipher.ALG_RSA_PKCS1_OAEP, false); + } catch (CryptoException e) { + if(e.getReason() == CryptoException.NO_SUCH_ALGORITHM) { + rsaOaepCipher = null; + } else { + throw e; + } + } + rsaRawCipher = Cipher.getInstance(Cipher.ALG_RSA_NOPAD, false); + + byte mechanisms = (byte) 0xC0; + fs = new GidsFileSystem(pinManager, transmitManager, (short) 0x3F00, + // FCP + new byte[] { + (byte)0x62, (byte)0x08, + (byte)0x82, (byte)0x01, (byte)0x38, // File descriptor byte. + (byte)0x8C, (byte)0x03, (byte)0x03, (byte)0x30, (byte)0x30,// security attribute + }, + // FCI + new byte[] { + 0x61, 0X12, + 0x4F, 0x0B, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x97, (byte) 0x42, (byte) 0x54, (byte) 0x46, (byte) 0x59, 0x02, 0x01, // AID + 0x73, 0x03, + 0x40, 0x01, mechanisms, // cryptographic mechanism + }, + // FMD + new byte[] { + (byte)0x64, (byte)0x09, + (byte)0x5F, (byte)0x2F, (byte) 0x01, (byte) 0x60, // pin usage policy + (byte)0x7F, (byte)0x65, 0x02, (byte) 0x80, 0x00 + } + ); + + // FCI / FMD / FCP are hard coded + register(); + } + + /** + * \brief This method is called whenever the applet is being deselected. + */ + @Override + public void deselect() { + pinManager.DeauthenticateAllPin(); + } + + /** + * \brief Processes an incoming APDU. + * + * \see APDU. + * + * \param apdu The incoming APDU. + */ + @Override + public void process(APDU apdu) { + byte buffer[] = apdu.getBuffer(); + byte ins = buffer[ISO7816.OFFSET_INS]; + + // No secure messaging at the moment + if(apdu.isSecureMessagingCLA()) { + ISOException.throwIt(ISO7816.SW_SECURE_MESSAGING_NOT_SUPPORTED); + } + + transmitManager.processChainInitialization(apdu); + + if(apdu.isISOInterindustryCLA()) { + switch (ins) { + case INS_ACTIVATE_FILE: + fs.processActivateFile(apdu); + break; + case INS_CREATE_FILE: + fs.processCreateFile(apdu); + break; + case INS_CHANGE_REFERENCE_DATA: + pinManager.processChangeReferenceData(apdu); + break; + case INS_DELETE_FILE: + fs.processDeleteFile(apdu); + break; + case INS_GENERAL_AUTHENTICATE: + pinManager.processGeneralAuthenticate(apdu); + break; + case INS_GENERATE_ASYMMETRIC_KEYPAIR: + processGenerateAsymmetricKeypair(apdu); + break; + case INS_GET_DATA: + processGetData(apdu); + break; + case INS_GET_RESPONSE: + transmitManager.processGetResponse(apdu); + break; + case INS_MANAGE_SECURITY_ENVIRONMENT: + processManageSecurityEnvironment(apdu); + break; + case INS_PERFORM_SECURITY_OPERATION: + processPerformSecurityOperation(apdu); + break; + case INS_PUT_DATA: + processPutData(apdu); + break; + case INS_RESET_RETRY_COUNTER: + pinManager.processResetRetryCounter(apdu); + break; + case ISO7816.INS_SELECT: + fs.processSelectFile(apdu, selectingApplet()); + break; + case INS_TERMINATE_DF: + processTerminateDF(apdu); + break; + case INS_VERIFY: + pinManager.processVerify(apdu); + break; + default: + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } // switch + } else { + ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); + } + } + + + + private void processTerminateDF(APDU apdu) { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + + if (p1 != (byte) 0x00 || p2 != (byte) 0x00 ) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + fs.CheckPermission(pinManager, File.ACL_OP_DF_TERMINATE); + // kill me + fs.setState(File.STATE_TERMINATED); + } + + /** + * \brief Process the GET DATA apdu (INS = CA) + * + * This APDU can be used to request the following data: + * P1P2 = 0x1001: Applet version and features + * + * \param apdu The apdu to process. + */ + private void processGetData(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + + if (p1 == 0x3F && p2 == (byte) 0xFF) { + // get Applet information + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + // check for public key request + // typically 00 CB 3F FF 0A 70 08 84 01 **81** A5 03 7F 49 80 00 (*keyref*) + if (lc == (short) 10 && buf[5] == (byte) 0x70) { + CRTKeyFile file = null; + byte keyID = buf[9]; + try { + file = fs.findKeyCRT(keyID); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + file.CheckPermission(pinManager, File.ACL_OP_KEY_GETPUBLICKEY); + PublicKey pk = file.GetKey().getPublic(); + + // Return pubkey. See ISO7816-8 table 3. + try { + sendPublicKey(apdu, pk); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_UNKNOWN); + } catch (NotEnoughSpaceException e) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + } else if (lc == (short) 04 && buf[5] == (byte) 0x5C && buf[6] == (byte) 0x02) { + short id = Util.makeShort(buf[7], buf[8]); + if (id == (short) 0x7F71 || id == (short) 0x7F72 || id == (short) 0x7F73 ) { + pinManager.returnPINStatus(apdu, id); + } else { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + } else { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + } else { + // read BER TLV DO + fs.processGetData(apdu); + } + + } + + + /** + * \brief Process the GENERATE ASYMMETRIC KEY PAIR apdu (INS = 46). + * + * A MANAGE SECURITY ENVIRONMENT must have succeeded earlier to set parameters for key + * generation. + * + * \param apdu The apdu. + * + * \throw ISOException SW_WRONG_LENGTH, SW_INCORRECT_P1P2, SW_CONDITIONS_NOT_SATISFIED, + * SW_SECURITY_STATUS_NOT_SATISFIED. + */ + public void processGenerateAsymmetricKeypair(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc, offset_cdata, pos, len, innerOffset, innerLength; + byte algID=0, keyID=0; + CRTKeyFile file = null; + KeyPair kp = null; + + // Check INS: We only support INS=D6 at the moment. + if (p1 != (byte) 0x00 || p2 != (byte) 0x00 ) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + // TLV structure consistency check. + if( ! UtilTLV.isTLVconsistent(buf, offset_cdata, lc)) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + // length and length-field of outer FCI tag consistency check. + try { + innerLength = UtilTLV.decodeLengthField(buf, (short)(offset_cdata+1)); + if(innerLength != (short)(lc-1-UtilTLV.getLengthFieldLength(buf, (short)(offset_cdata+1)))) { + throw InvalidArgumentsException.getInstance(); + } + + // Let innerOffset point to the first inner TLV entry. + innerOffset = (short) (offset_cdata + 1 + UtilTLV.getLengthFieldLength(buf, (short)(offset_cdata+1))); + + // Now we check for the consistency of the lower level TLV entries. + if( ! UtilTLV.isTLVconsistent(buf, innerOffset, innerLength) ) { + throw InvalidArgumentsException.getInstance(); + } + pos = UtilTLV.findTag(buf, innerOffset, innerLength, (byte) 0x83); + len = UtilTLV.decodeLengthField(buf, (short)(pos+1)); + if (len != (short) 1) { + throw InvalidArgumentsException.getInstance(); + } + keyID = buf[(short)(pos+2)]; + pos = UtilTLV.findTag(buf, innerOffset, innerLength, (byte) 0x80); + len = UtilTLV.decodeLengthField(buf, (short)(pos+1)); + if (len != (short) 1) { + throw InvalidArgumentsException.getInstance(); + } + algID = buf[(short)(pos+2)]; + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try { + file = fs.findKeyCRT(keyID); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + file.CheckPermission(pinManager, File.ACL_OP_KEY_GENERATE_ASYMETRIC); + try { + switch(algID) { + case (byte)0x06: + kp = new KeyPair(KeyPair.ALG_RSA_CRT, KeyBuilder.LENGTH_RSA_1024); + break; + case (byte)0x07: + kp = new KeyPair(KeyPair.ALG_RSA_CRT, KeyBuilder.LENGTH_RSA_2048); + break; + default: + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + break; + } + kp.genKeyPair(); + } catch(CryptoException e) { + if(e.getReason() == CryptoException.NO_SUCH_ALGORITHM) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + ISOException.throwIt(ISO7816.SW_UNKNOWN); + } + file.SaveKey(kp); + + // Return pubkey. See ISO7816-8 table 3. + try { + sendPublicKey(apdu, kp.getPublic()); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_UNKNOWN); + } catch (NotEnoughSpaceException e) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + } + + + + private void sendPublicKey(APDU apdu, PublicKey publicKey) throws InvalidArgumentsException, NotEnoughSpaceException { + + if (publicKey instanceof RSAPublicKey) { + sendRSAPublicKey(apdu, (RSAPublicKey) publicKey); + } + } + + /** + * \brief Encode a 2048 bit RSAPublicKey according to ISO7816-8 table 3 and send it as a response, + * using an extended APDU. + * + * \see ISO7816-8 table 3. + * + * \param apdu The apdu to answer. setOutgoing() must not be called already. + * + * \param key The RSAPublicKey to send. + * Can be null for the secound part if there is no support for extended apdus. + */ + private void sendRSAPublicKey(APDU apdu, RSAPublicKey key) { + + short pos = 0; + short size = key.getSize(); + byte[] ram_buf = transmitManager.GetRamBuffer(); + transmitManager.ClearRamBuffer(); + + ram_buf[pos++] = (byte) 0x7F; // Interindustry template for nesting one set of public key data objects. + ram_buf[pos++] = (byte) 0x49; // " + + if (size < (short) 2048) { + ram_buf[pos++] = (byte) 0x81; // Length field: 2 Bytes. + ram_buf[pos++] = (byte) ((size / 8) + 8); + } else { + ram_buf[pos++] = (byte) 0x82; // Length field: 3 Bytes. + Util.setShort(ram_buf, pos, (short)((size / 8) + 9)); + pos += 2; + } + + ram_buf[pos++] = (byte) 0x81; // RSA public key modulus tag. + if (size < (short) 2048) { + ram_buf[pos++] = (byte) 0x81; // Length field: 2 Bytes. + ram_buf[pos++] = (byte) (size / 8); + } else { + ram_buf[pos++] = (byte) 0x82; // Length field: 3 Bytes. + Util.setShort(ram_buf, pos, (short)(size / 8)); + pos += 2; + } + pos += key.getModulus(ram_buf, pos); + ram_buf[pos++] = (byte) 0x82; // RSA public key exponent tag. + ram_buf[pos++] = (byte) 0x03; // Length: 3 Bytes. + pos += key.getExponent(ram_buf, pos); + + transmitManager.sendDataFromRamBuffer(apdu, (short)0, pos); + } + + + + /** + * \brief Process the MANAGE SECURITY ENVIRONMENT apdu (INS = 22). + * + * \attention Only SET is supported. RESTORE will reset the security environment. + * The security environment will be cleared upon deselection of the applet. + * STOREing and ERASEing of security environments is not supported. + * + * \param apdu The apdu. + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED, SW_WRONG_LENGTH, SW_DATA_INVALID, + * SW_INCORRECT_P1P2, SW_FUNC_NOT_SUPPORTED, SW_COMMAND_NOT_ALLOWED. + */ + public void processManageSecurityEnvironment(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short pos = 0; + short offset_cdata; + byte algRef = 0; + byte privKeyRef = -1; + CRTKeyFile crt = null; + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + // TLV structure consistency check. + if( ! UtilTLV.isTLVconsistent(buf, offset_cdata, lc)) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + /* Extract data: */ + switch(p1) { + case (byte) 0x81: + // SET Verification, encipherment, external authentication and key agreement. + case (byte) 0xC1: + // Private key reference (Index in keys[]-array). + try { + pos = UtilTLV.findTag(buf, offset_cdata, (byte) lc, (byte) 0x83); + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if(buf[++pos] != (byte) 0x01 ) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + privKeyRef = buf[++pos]; + algRef = (byte) 0x02; + break; + case (byte) 0x41: + // SET Computation, decipherment, internal authentication and key agreement. + + // Algorithm reference. + try { + pos = UtilTLV.findTag(buf, offset_cdata, (byte) lc, (byte) 0x80); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if(buf[++pos] != (byte) 0x01) { // Length must be 1. + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + // Set the current algorithm reference. + algRef = buf[++pos]; + + // Private key reference (Index in keys[]-array). + try { + pos = UtilTLV.findTag(buf, offset_cdata, (byte) lc, (byte) 0x84); + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if(buf[++pos] != (byte) 0x01) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + privKeyRef = buf[++pos]; + break; + + case (byte) 0xF3: + // RESTORE // Set sec env constants to default values. + algRef = 0; + privKeyRef = -1; + break; + + case (byte) 0xF4: // ERASE + case (byte) 0xF2: // STORE + default: + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + + if(privKeyRef == -1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try { + crt = fs.findKeyCRT(privKeyRef); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + try { + crt.CheckUsage(p2, algRef); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + crt.CheckPermission(pinManager, File.ACL_OP_KEY_MANAGE_SEC_ENV); + + if (p1 == (byte) 0xc1 || p1 == (byte) 0x81) { + pinManager.SetKeyReference(crt); + } else { + pinManager.SetKeyReference(null); + } + + // Finally, update the security environment. + JCSystem.beginTransaction(); + currentAlgorithmRef[0] = algRef; + currentKey[0] = crt; + JCSystem.commitTransaction(); + + } + + /** + * \brief Process the PERFORM SECURITY OPERATION apdu (INS=2A). + * + * This operation is used for cryptographic operations + * (Computation of digital signatures, decrypting.). + * + * \param apdu The PERFORM SECURITY OPERATION apdu. + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED, SW_INCORRECT_P1P2 and + * the ones from computeDigitalSignature() and decipher(). + */ + private void processPerformSecurityOperation(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + + if(p1 == (byte) 0x9E && p2 == (byte) 0x9A) { + computeDigitalSignature(apdu); + } else if(p1 == (byte) 0x80 && p2 == (byte) 0x86) { + decipher(apdu); + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + } + + /** + * \brief Decipher the data from the apdu using the private key referenced by + * an earlier MANAGE SECURITY ENVIRONMENT apdu. + * + * \param apdu The PERFORM SECURITY OPERATION apdu with P1=80 and P2=86. + * + * \throw ISOException SW_CONDITIONS_NOT_SATISFIED, SW_WRONG_LENGTH and + * SW_WRONG_DATA + */ + private void decipher(APDU apdu) { + byte[] buf = apdu.getBuffer(); + short offset_cdata; + short lc; + short decLen = -1; + byte[] ram_buf = transmitManager.GetRamBuffer(); + Cipher cipher = null; + + lc = transmitManager.doChainingOrExtAPDU(apdu); + offset_cdata = 0; + + // Padding indicator should be "No further indication". + if(buf[offset_cdata] != (byte) 0x00) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + switch((byte) (currentAlgorithmRef[0] & 0xF0)) { + + case (byte) 0x80: + cipher = rsaOaepCipher; + break; + case (byte) 0x40: + cipher = rsaPkcs1Cipher; + break; + case (byte) 0x00: + cipher = rsaRawCipher; + break; + default: + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + // Get the key - it must be an RSA private key, + // checks have been done in MANAGE SECURITY ENVIRONMENT. + CRTKeyFile key = (CRTKeyFile) currentKey[0]; + PrivateKey theKey = key.GetKey().getPrivate(); + + // Check the length of the cipher. + // Note: The first byte of the data field is the padding indicator + // and therefor not part of the ciphertext. + if(lc != (short)(theKey.getSize() / 8)) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + + cipher.init(theKey, Cipher.MODE_DECRYPT); + + try { + decLen = cipher.doFinal(ram_buf, (short) 0, lc, + buf, (short) 0); + } catch(CryptoException e) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + // We have to send at most 256 bytes. A short APDU can handle that - only one send operation neccessary. + apdu.setOutgoingAndSend((short)0, decLen); + } + + /** + * \brief Compute a digital signature of the data from the apdu + * using the private key referenced by an earlier + * MANAGE SECURITY ENVIRONMENT apdu. + * + * \attention The apdu should contain a hash, not raw data for RSA keys. + * PKCS1 padding will be applied if neccessary. + * + * \param apdu The PERFORM SECURITY OPERATION apdu with P1=9E and P2=9A. + * + * \throw ISOException SW_CONDITIONS_NOT_SATISFIED, SW_WRONG_LENGTH + * and SW_UNKNOWN. + */ + private void computeDigitalSignature(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + short offset_cdata; + short lc, le; + short sigLen = 0; + PrivateKey rsaKey = null; + byte[] ram_buf = transmitManager.GetRamBuffer(); + CRTKeyFile key = (CRTKeyFile) currentKey[0]; + + switch((byte) (currentAlgorithmRef[0] & 0xF0)) { + case (byte) 0x10: + // padding made off card -> raw encryption to be performed + lc = transmitManager.doChainingOrExtAPDU(apdu); + + // RSA signature operation. + rsaKey = key.GetKey().getPrivate(); + + rsaRawCipher.init(rsaKey, Cipher.MODE_ENCRYPT); + sigLen = rsaRawCipher.doFinal(ram_buf, (short) 0, lc, ram_buf, (short)0); + // A single short APDU can handle 256 bytes - only one send operation neccessary. + le = apdu.setOutgoing(); + if(le < sigLen) { + ISOException.throwIt(ISO7816.SW_CORRECT_LENGTH_00); + } + apdu.setOutgoingLength(sigLen); + apdu.sendBytesLong(ram_buf, (short) 0, sigLen); + break; + case (byte) 0x50: + // rsa padding made by the card, only the hash is provided + + // Receive. + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + // RSA signature operation. + rsaKey = key.GetKey().getPrivate(); + + if(lc > (short) 247) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + + rsaPkcs1Cipher.init(rsaKey, Cipher.MODE_ENCRYPT); + sigLen = rsaPkcs1Cipher.doFinal(buf, offset_cdata, lc, ram_buf, (short)0); + + /*if(sigLen != 256) { + ISOException.throwIt(ISO7816.SW_UNKNOWN); + }*/ + + // A single short APDU can handle 256 bytes - only one send operation neccessary. + le = apdu.setOutgoing(); + if(le < sigLen) { + ISOException.throwIt(ISO7816.SW_CORRECT_LENGTH_00); + } + apdu.setOutgoingLength(sigLen); + apdu.sendBytesLong(ram_buf, (short) 0, sigLen); + break; + + default: + // Wrong/unknown algorithm. + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + } + + /** + * \brief Process the PUT DATA apdu (INS=DB). + * + * PUT DATA is currently used for private key import. + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED, SW_INCORRECT_P1P2 + */ + private void processPutData(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + + if(p1 == (byte) 0x3F && p2 == (byte) 0xFF) { + importPrivateKey(apdu); + } else { + fs.processPutData(apdu); + } + } + + /** + * \brief Upload and import a usable private key. + * + * A preceeding MANAGE SECURITY ENVIRONMENT is necessary (like with key-generation). + * The format of the data (of the apdu) must be BER-TLV, + * Tag 7F48 ("T-L pair to indicate a private key data object") for RSA or tag 0xC1 + * for EC keys, containing the point Q. + * + * For RSA, the data to be submitted is quite large. It is required that command chaining is + * used for the submission of the private key. One chunk of the chain (one apdu) must contain + * exactly one tag (0x92 - 0x96). The first apdu of the chain must contain the outer tag (7F48). + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED, SW_DATA_INVALID, SW_WRONG_LENGTH. + */ + private void importPrivateKey(APDU apdu) throws ISOException { + short recvLen; + short len = 0, pos = 0; + short innerPos = 0, innerLen = 0; + byte[] ram_buf = transmitManager.GetRamBuffer(); + byte privKeyRef = -1; + CRTKeyFile crt = null; + + if( ! DEF_PRIVATE_KEY_IMPORT_ALLOWED) { + ISOException.throwIt(ErrorCode.SW_COMMAND_NOT_ALLOWED_GENERAL); + } + + recvLen = transmitManager.doChainingOrExtAPDU(apdu); + + try { + innerPos = UtilTLV.findTag(ram_buf, (short) 0, recvLen, (byte) 0x70); + innerLen = UtilTLV.decodeLengthField(ram_buf, (short)(innerPos+1)); + innerPos += 1 + UtilTLV.getLengthFieldLength(ram_buf, (short)(innerPos+1)); + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + try { + pos = UtilTLV.findTag(ram_buf, innerPos, innerLen, (byte) 0x84); + len = UtilTLV.decodeLengthField(ram_buf, (short)(innerPos+1)); + if (len != 1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + privKeyRef = ram_buf[(short) (pos+2)]; + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try { + pos = UtilTLV.findTag(ram_buf, innerPos, innerLen, (byte) 0xA5); + len = UtilTLV.decodeLengthField(ram_buf, (short)(innerPos+1)); + } catch (Exception e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if(privKeyRef == -1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try { + crt = fs.findKeyCRT(privKeyRef); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + crt.CheckPermission(pinManager, File.ACL_OP_KEY_PUTKEY); + + try { + crt.importKey(ram_buf, pos, len); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + } + +} // class GidsApplet diff --git a/src/com/mysmartlogon/gidsApplet/GidsFileSystem.java b/src/com/mysmartlogon/gidsApplet/GidsFileSystem.java new file mode 100644 index 0000000..e6209bf --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/GidsFileSystem.java @@ -0,0 +1,833 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.*; + +/** + * \brief The ISO 7816 compliant GidsFileSystem class. + * + * It is the root of the file structure and is therefor equivalent to the ISO Master File (MF). + * Normally, most of the file system oriented operations should happen through one object of this class. + * + * Due to the ISO 7816-4 DF and EF selection (see section 7.1) the currently selected DF and EF + * are being saved internally. File-related operations are being executed upon those selected files respectively. + * It is therefor possible to select a file and execute a number of operations upon this file without the need to + * specify a target in each individual method call. This also saves execution time and reduces stack usage. + * + * \author Philip Wendland + */ +public class GidsFileSystem extends ApplicationFile { + /* Additional ISO Status Words */ + + public static final short SW_OFFSET_OUTSIDE_EF = 0x6B00; + + public static final byte OFFSET_CURRENT_DF = 0; + public static final byte OFFSET_CURRENT_EF = 1; + + private Object[] currentlySelectedFiles = null; + short currentRecordNum; + private TransmitManager transmitManager = null; + public static final short AFDID = 0x3FFF; + + private GidsPINManager pinManager = null; + /** + * \brief Instantiate a new ISO 7816 compliant GidsFileSystem. + * + * The GidsFileSystem class should normally only be instanciated once. It represents the file system and + * is therefor equivalemt to the ISO Master File (MF). + * Most of the file system related operations are performed through the returned object. + * + * \see GidsFileSystem. + * + * \param fileID The file ID of the master file. Should be 0x3F00 as specified by ISO. + * + * \param fileControlInformation The FCI according to ISO 7816-4 table 12. Necessary tags: 82, 83. No copy is made. + */ + public GidsFileSystem(GidsPINManager pinManager, TransmitManager transmitManager, short fileID, byte[] fileControlParameter, byte[] fileControlInformation, byte[] fileManagementData) { + super(fileID, fileControlParameter, fileControlInformation, fileManagementData); + this.currentRecordNum = 0; + this.currentlySelectedFiles = JCSystem.makeTransientObjectArray((short) 2, JCSystem.CLEAR_ON_DESELECT); + this.currentlySelectedFiles[OFFSET_CURRENT_DF] = this; + this.pinManager = pinManager; + this.transmitManager = transmitManager; + // file system is in creation state + } + + + /** + * \brief Get the currently selected DF. + * + * \return The currently selected DF. + */ + public DedicatedFile getCurrentlySelectedDF() { + return ((DedicatedFile)currentlySelectedFiles[OFFSET_CURRENT_DF]); + } + + /** + * \brief Set the currently selected DF. + * + * \param fileID The ID of the file. + * + * \throw NotFoundException If the specified file was not found or was of the wrong type. + */ + public void setCurrentlySelectedDF(short fileID) throws NotFoundException { + selectFile( findFile(fileID, SPECIFY_DF) ); + return; + } + + /** + * \brief Get the currently selected Elementary File. + * + * \return The currently selected EF. + */ + public ElementaryFile getCurrentlySelectedEF() { + return ((ElementaryFile)currentlySelectedFiles[OFFSET_CURRENT_EF]); + } + + /** + * \brief Set the currently selected Elementary File. + * + * \brief fileID The ID of the file. + * + * \throw NotFoundException If the specified file was not found or was of the wrong type. + */ + public void setCurrentlyselectedEF(short fileID) throws NotFoundException { + selectFile( findFile(fileID, SPECIFY_EF) ); + return; + } + + /** + * \brief Get the number of the current record for the currently selected EF. + * + * \return The record number. + */ + public short getCurrentRecordNumber() { + return currentRecordNum; + } + + /** + * \brief Check wether the operation is valid according to the security status of the + * filesystem. + * + * If the operation is permitted, this method just returns. If not, it throws an + * ISOException with a SECURITY STATUS NOT SATISFIED status word. The processing + * of the current APDU will be aborted. + * + * \param file The file the opertaion is executed upon. + * + * \param flag_operation A flag of ACL_OP_* to specify the operation. + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED if the operation is not + * permitted. + */ + public void authenticateAction(File file, byte flag_operation) throws ISOException { + if (file == null) { + return; + } + file.CheckPermission(pinManager, flag_operation); + } + + /** + * \brief Search for the DF with the specified name. + * + * \param dfName The array containing the up to 16 byte long DedicatedFile name. + * + * \param nameOffset The offset at which the DF name begins in the name array. + * + * \param nameLength The length of the DF name. + * + * \throw NotFoundException If the file was not found. + * + * \return The requested DedicatedFile (if found). + */ + public DedicatedFile findDedicatedFileByName(byte[] dfName, short nameOffset, short nameLength) throws NotFoundException { + if (isName(dfName, nameOffset, nameLength)) { + return this; + } + return super.findDedicatedFileByNameRec(dfName, nameOffset, nameLength); + } + + + /** + * \brief find the file with the specified file ID. + * + * \param fileID the ID of the file. + * + * \param flag A flag to specify if the currently selected EF or DF is the target (SPECIFY_EF, SPECIFY_DF, SPECIFY_ANY). + * + * \throw NotFoundException If the file could not be found. + * + * \return The File (if found). + */ + public File findFile(short fileID, byte flag) throws NotFoundException { + if(fileID == getFileID() && flag != SPECIFY_EF) { + return this; + } + return super.findChildrenRec(fileID, flag); + } + + public CRTKeyFile findKeyCRT(byte keyID) throws NotFoundException { + File file = findFile(Util.makeShort((byte)0xB0, keyID), SPECIFY_EF); + if (!(file instanceof CRTKeyFile)) { + throw NotFoundException.getInstance(); + } + return (CRTKeyFile) file; + } + + /** + * \brief Set the given file as the selected. + * + * If the file is a DedicatedFile, only the currently selected DF is changed. + * In case of an ElementaryFile the currently selected EF will be the file specified and the + * currently selected DF will become its parent according to ISO 7816-4, section 7.1.1. + * + * \param file The file to select. Must be of DedicatedFile, GidsFileSystem or any subclass of ElementaryFile. + * It should be member of the file system hierarchy (not checked). + */ + public void selectFile(File file) { + if(file == null) { + currentlySelectedFiles[OFFSET_CURRENT_DF] = this; + currentlySelectedFiles[OFFSET_CURRENT_EF] = null; + } else if(file instanceof DedicatedFile) { + currentlySelectedFiles[OFFSET_CURRENT_DF] = file; + currentlySelectedFiles[OFFSET_CURRENT_EF] = null; + } else if (file instanceof ElementaryFile) { + currentlySelectedFiles[OFFSET_CURRENT_EF] = file; + currentlySelectedFiles[OFFSET_CURRENT_DF] = ((ElementaryFile)currentlySelectedFiles[OFFSET_CURRENT_EF]).getParentDF(); + this.currentRecordNum = 0; + } + return; + } + + /** + * \brief Add a file to the currently selected DedicatedFile. + * + * The currently selected DF becomes the parent of the file. + * The DF's child and the EF's parent relation is being updated. + * + * \param file A reference of the file to save. + * + * \throw NotEnoughSpaceException If the maximum amount of + * children would have been exceeded. + */ + public void addFile(File file) throws NotEnoughSpaceException { + file.setParentDF(getCurrentlySelectedDF()); + getCurrentlySelectedDF().addChildren(file); + return; + } + + + /** + * \brief "Safely" instantiate a File according to the provided File Control Information. + * + * Used by processCreateFile(). + * + * \callergraph + * + * \param fci The array containing the file control information (FCI) according to + * ISO7816-4 table 12. Mandatory Tags: 82, 83. A copy of the FCI will be + * made for the new file. + * + * \param offset The offset of the FCI information in the array. + * + * \param length The length of the FCI information. Should be consistent with the length + * field if the FCI (6F) tag. + * + * \throw ISOException SW_SECURITY_STATUS_NOT_SATISFIED. + * + * \return The new file of the FCI was valid, null else. + */ + public File getSafeFile(byte[] fci, short offset, short length) throws ISOException, InvalidArgumentsException, NotFoundException { + short fileID; + byte fileDescByte; + final short innerLength, innerOffset; + short pos, len; + + /* ********************** + * Check FCI structure. * + ************************/ + // Are we in bounds? + if((short)(fci.length) <= (short)(offset+length)) { + throw InvalidArgumentsException.getInstance(); + } + + // FCI must begin with tag "6F". Or we have FCP, tag "62". + if(fci[(offset)] != (byte) 0x6F + && fci[(offset)] != (byte) 0x62) { + throw NotFoundException.getInstance(); + } + + // length and length-field of outer FCI tag consistency check. + innerLength = UtilTLV.decodeLengthField(fci, (short)(offset+1)); + if(innerLength != (short)(length-1-UtilTLV.getLengthFieldLength(fci, (short)(offset+1)))) { + throw InvalidArgumentsException.getInstance(); + } + + // Let innerOffset point to the first inner TLV entry. + innerOffset = (short) (offset + 1 + UtilTLV.getLengthFieldLength(fci, (short)(offset+1))); + + // Now we check for the consistency of the lower level TLV entries. + if( ! UtilTLV.isTLVconsistent(fci, innerOffset, innerLength) ) { + throw InvalidArgumentsException.getInstance(); + } + + // Extract the FID from the FCI which is passed to the FileXXX contructor and saved + // separately for performance reasons. + pos = UtilTLV.findTag(fci, innerOffset, innerLength, (byte) 0x83); + len = UtilTLV.decodeLengthField(fci, (short)(pos+1)); + if (len != (short) 2) { + throw InvalidArgumentsException.getInstance(); + } + fileID = Util.getShort(fci, (short)(pos+1+UtilTLV.getLengthFieldLength(fci, (short)(pos+1)))); + // The fileID must be unique. + try { + this.findFile(fileID, SPECIFY_ANY); + throw InvalidArgumentsException.getInstance(); + } catch(NotFoundException e) { + + } + + // Check and get the File Descriptor Byte (ISO 7816-4 table 14). + pos = UtilTLV.findTag(fci, innerOffset, innerLength, (byte) 0x82); + len = UtilTLV.decodeLengthField(fci, (short)(pos+1)); + // Ensure position found and correct length: + if(len < (short)1 || len > (short)6) { + throw InvalidArgumentsException.getInstance(); + } + fileDescByte = fci[(short)(pos+2)]; + + byte[] fciEEPROM = null; + if(fileDescByte == 0x39) { + // BER-TLV + // Check the permissions. + authenticateAction(((DedicatedFile)currentlySelectedFiles[OFFSET_CURRENT_DF]), ACL_OP_DF_CREATE_EF); + fciEEPROM = new byte[length]; + Util.arrayCopy(fci, offset, fciEEPROM, (short) 0, length); + return new BerTlvFile(fileID, fciEEPROM); + } else if(fileDescByte == 0x18) { + // key file descriptor + // Check if there is a CRT template + + // Search the CRT tag (A5). If not found, then raise an error + try { + pos = UtilTLV.findTag(fci, innerOffset, innerLength, (byte) 0xA5); + len = UtilTLV.decodeLengthField(fci, (short)(pos+1)); + } catch (NotFoundException e) { + throw InvalidArgumentsException.getInstance(); + } + CRTKeyFile.CheckCRT(fci, pos, len); + authenticateAction(((DedicatedFile)currentlySelectedFiles[OFFSET_CURRENT_DF]), ACL_OP_DF_CREATE_EF); + fciEEPROM = new byte[length]; + Util.arrayCopy(fci, offset, fciEEPROM, (short) 0, length); + return new CRTKeyFile(fileID, fciEEPROM, (short) (pos - offset), len); + } else { + // Not a supported file format. + throw InvalidArgumentsException.getInstance(); + } + } + + + + /* ************************************** + * processXXX methods for ISO commands: * + ****************************************/ + + /* ISO 7816-4 */ + + /** + * \brief Process the SELECT (FILE) apdu. + * + * This method updates the currently selected EF or DF, according to the parameters in the apdu. + * Every selection method according to ISO 7816-4 Table 39 is valid. + * There are limitations of the P2 byte (b8...b1) at the moment, however: + * - The first or only occurence is the only supported file occurence (b2b1 = 00) + * - No FMD is returned. (b4b3 != 10, if b4b3 = 00 then the response only contains the FCP template.) + * + * \param apdu The SELECT (FILE) apdu + * + * \throw ISOException SW_INCORRECT_P1P2 and SW_FILE_NOT_FOUND. + */ + public void processSelectFile(APDU apdu, boolean selectingApplet) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short offset_cdata; + short fid; + File fileToSelect = null; + + if (selectingApplet) { + fileToSelect = this; + } else { + + // Only "first or only occurence" supported at the moment (ISO 7816-4 Table 40). + if((p2 & 0xF3) != 0x00) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + // Select the file. + switch(p1) { + case 0x00: /* MF, DF or EF using FID */ + if(lc == 0) { + fileToSelect = this; + } else if(lc == 2) { + // we have a FID + fid = Util.makeShort(buf[offset_cdata], buf[(short)(offset_cdata+1)]); + if (fid == AFDID) { + fileToSelect = this; + } else { + try { + fileToSelect = findFile(fid , SPECIFY_ANY); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + } + } + break; + case 0x01: /* Child DF unsing "DF identifier" (i.e. ID of a DF) */ + fid = Util.makeShort(buf[offset_cdata], buf[(short)(offset_cdata+1)]); + try { + fileToSelect = getCurrentlySelectedDF().findChildrenRec(fid, SPECIFY_DF); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + case 0x02: /* EF under the current DF using "EF identifer" (i.e. FID of a EF) */ + fid = Util.makeShort(buf[offset_cdata], buf[(short)(offset_cdata+1)]); + try { + fileToSelect = getCurrentlySelectedDF().findChildrenRec(fid, SPECIFY_EF); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + case 0x03: /* parent DF of the current DF */ + if(lc != 0) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + // The MF ("this") has no parent. + if(getCurrentlySelectedDF() != this) { + fileToSelect = getCurrentlySelectedDF().getParentDF(); + } else { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + case 0x04: /* by DF name */ + try { + fileToSelect = findDedicatedFileByName(buf, offset_cdata, lc); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + case 0x08: /* Path from MF */ + try { + fileToSelect = this.findChildrenByPath(buf, offset_cdata, lc); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + case 0x09: /* Path from current DF */ + try { + fileToSelect = getCurrentlySelectedDF().findChildrenByPath(buf, offset_cdata, lc); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + break; + default: + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + } + + selectFile( fileToSelect ); + + /* + * The file is selected now. We still have to check P2 to see if we need to return any FCI/FCP/FMD information. + * If we have to, we can use the apdu buffer to save the TLV encoded entries as that is what we want to send back anyway (for performance reasons). + * We don't use javacardx.framework.tlv.BERTLV as smartcard support is scarce.. + */ + lc = 0; // We re-use lc here for the length of the response data. + switch(p2 & 0xFC) { + case 0x00: + /* Return FCI. */ + if (fileToSelect instanceof ApplicationFile) { + byte[] fci = ((ApplicationFile) fileToSelect).getFileControlInformation(); + if(fci != null) { + Util.arrayCopy(fci, (short) 0, buf, (short) 0, (short) (fci.length)); + lc += (short) (fci.length); + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + break; + case 0x04: + // Return FCP. + if(fileToSelect.getFileControlParameter() != null) { + Util.arrayCopy(fileToSelect.getFileControlParameter(), (short) 0, buf, (short) 0, (short) fileToSelect.getFileControlParameter().length); + lc += (short) fileToSelect.getFileControlParameter().length; + } + break; + case 0x08: + // Return FMD. + if (fileToSelect instanceof ApplicationFile) { + byte[] fmd = ((ApplicationFile) fileToSelect).getFileManagementData(); + if(fmd != null) { + Util.arrayCopy(fmd, (short) 0, buf, (short) 0, (short) fmd.length); + lc += (short) fmd.length; + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + break; + case 0x0C: + // Return nothing. + break; + } + if( lc > 0) { + apdu.setOutgoingAndSend((short) 0, lc); + } + return; + } + + + /** + * \brief Process the DELETE FILE apdu. + * + * \attention Only deletion by FID is supported. Lc must be 2, the DATA field + * must contain the file ID. P1P2 must be 0000. + * + * \todo Add support for other file identification methods as in SELECT. + * + * \param apdu The DELETE FILE apdu. + * + * \throw ISOException SW_INCORRECT_P1P2, SW_WRONG_LENGTH, SW_FILE_NOT_FOUND and + * SW_SECURITY_STATUS_NOT_SATISFIED. + */ + public void processDeleteFile(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + File fileToDelete = null; + + // Only P1P2 = 0000 is currently supported. + // (File identifier must be encoded in the command data field.) + if( p1 != 0x00 || p2 != 0x00 ) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + + // One FID in DATA. + if (lc == 0) { + fileToDelete = getCurrentlySelectedEF(); + if (fileToDelete == null) { + fileToDelete = getCurrentlySelectedDF(); + } + } else { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + // Don't delete the MF. + if(fileToDelete == this) { + ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED); + } + + // Permissions. + authenticateAction(fileToDelete, ACL_OP_EF_DELETE); + + // Update current DF before deletion. + currentlySelectedFiles[OFFSET_CURRENT_DF] = (fileToDelete.getParentDF()); + currentlySelectedFiles[OFFSET_CURRENT_EF] = null; + + // Remove from tree. Garbage collector has already been called by deleteChildren(). + try { + getCurrentlySelectedDF().deleteChildren(fileToDelete.getFileID()); + } catch(NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + } + + /** + * \brief Process the CREATE FILE apdu. + * + * This method creates a file, adds it to the filesystem structure and selects it. + * Configuration options are taken from the DATA field of the APDU. (I.e. P1 and P2 must be 00.) + * The data field of the APDU must be 2-level nested TLV encoded. The upper level is the FCI (6F) or FCP (62) tag. + * The nested information will be added to the file as FCI. Also, the following information is being taken in + * order to allocate the right ressources: + * - The file ID (tag 83) + * - The file description byte (tag 82) to determine the type, also following information to determine record + * sizes and amounts in case of non-transparent EFs. + * - In the case of a transparent EF, the data size (excluding structural information) (tag 80) in order to + * allocate enough space. + * + * \param apdu The SELECT (FILE) apdu + * + * \throw ISOException SW_INCORRECT_P1P2, SW_DATA_INVALID, SW_FILE_FULL and SW_SECURITY_STATUS_NOT_SATISFIED. + */ + public void processCreateFile(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short offset_cdata; + + // Only P1P2 = 0000 supported. + // (File identifier and parameters must be encoded in the command data field.) + if( p1 != 0x00 || p2 != 0x00 ) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + try { + // Add the file to the filesystem and select it. + File fileToAdd = getSafeFile(buf, offset_cdata, lc); // getSafeFile performs permission checks. + addFile(fileToAdd); + selectFile(fileToAdd); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch(NotEnoughSpaceException e) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + + return; + } + + + public void processGetData(APDU apdu) { + byte[] buf = apdu.getBuffer(); + byte ins = buf[ISO7816.OFFSET_INS]; + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short offset_cdata; + short lc, pos = 0, len = 0, fileID; + File file = null; + BerTlvFile bertlvfile = null; + + if (/*ins != 0xCA ||*/ ins != (byte) 0xCB) { + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + if (p1 == 0x3F && p2 == (byte) 0xFF) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } else if (p1 == 0x00 && p2 == 0x00) { + file = getCurrentlySelectedEF(); + } else if (p1 == (byte) 0xFF && p2 == (byte) 0xFF) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } else { + fileID = Util.getShort(buf, (short) 2); + try { + file = findFile(fileID, SPECIFY_EF); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + } + + if(!(file instanceof BerTlvFile)) { + ISOException.throwIt(ErrorCode.SW_COMMAND_INCOMPATIBLE_WITH_FILE_STRUCTURE); + } + bertlvfile = (BerTlvFile) file; + + authenticateAction(file, ACL_OP_DO_GET_DATA); + + fileID = bertlvfile.getFileID(); + + if (fileID != (short) 0x2F00 && fileID != (short) 0x2F01 && fileID != (short) 0x3F00 && fileID != (short) 0x0000) { + // implicit selection not valid for every file + selectFile(bertlvfile); + } + try { + // Extract the FID from the FCI which is passed to the FileXXX contructor and saved + // separately for performance reasons. + pos = UtilTLV.findTag(buf, offset_cdata, (byte) lc, (byte) 0x5C); + len = UtilTLV.decodeLengthField(buf, (short)(pos+1)); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if ((len == 0) + || (len == 1 && buf[(short) (pos+2)]== (byte)0x5C)) { + transmitManager.sendRecords(apdu, bertlvfile.getAllData()); + } else { + try { + Record record = bertlvfile.getData(buf, (short)(pos+1+UtilTLV.getLengthFieldLength(buf, (short)(pos+1))), (short) (offset_cdata + lc)); + transmitManager.sendRecord(apdu,record); + } catch (NotFoundException e) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + } + } + + + public void processPutData(APDU apdu) { + byte[] buf = apdu.getBuffer(); + byte ins = buf[ISO7816.OFFSET_INS]; + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short offset_cdata, lc; + short fileID; + File file = null; + BerTlvFile bertlvfile = null; + short size; + Record record = null; + if (/*ins != 0xCA ||*/ ins != (byte) 0xDB) { + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } + + if (p1 == 0x3F && p2 == (byte) 0xFF) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } else if (p1 == 0x00 && p2 == 0x00) { + file = getCurrentlySelectedEF(); + } else if (p1 == (byte) 0xFF && p2 == (byte) 0xFF) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } else { + fileID = Util.getShort(buf, (short) 2); + try { + file = findFile(fileID, SPECIFY_EF); + } catch (NotFoundException e) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + } + + if(!(file instanceof BerTlvFile)) { + ISOException.throwIt(ErrorCode.SW_COMMAND_INCOMPATIBLE_WITH_FILE_STRUCTURE); + } + bertlvfile = (BerTlvFile) file; + + authenticateAction(file, ACL_OP_DO_PUT_DATA); + + selectFile(bertlvfile); + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + record = transmitManager.returnCachedRecord(); + if (record == null && TransmitManager.isCommandChainingCLA(apdu)) { + // handle first chained APDU + size = UtilTLV.CheckBERTLV(buf, offset_cdata, (short) (offset_cdata + lc)); + try + { + record = bertlvfile.addChildren(buf, offset_cdata, size, lc); + } catch(NotEnoughSpaceException e) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + transmitManager.setCachedRecord(record); + transmitManager.setCachedOffset(lc); + } else if (record != null) { + // handle next chained APDU + short offset = transmitManager.returnCachedOffset(); + byte[] data = record.GetData(); + if ((short) (offset + lc) > data.length) { + transmitManager.clearCachedRecord(); + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + Util.arrayCopyNonAtomic(buf, offset_cdata, data, offset, lc); + transmitManager.setCachedOffset((short) (offset + lc)); + if ((short) (offset + lc) == data.length) { + transmitManager.clearCachedRecord(); + } else if (!TransmitManager.isCommandChainingCLA(apdu)) { + // the data sent is too short + // clear it + transmitManager.clearCachedRecord(); + Util.arrayFillNonAtomic(data, (short) 0, (short) data.length, (byte) 0); + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + // else wait for the next record + } else { + size = UtilTLV.CheckBERTLV(buf, offset_cdata, (short) (offset_cdata + lc)); + if (size <= 0) + { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + try + { + bertlvfile.addChildren(buf, offset_cdata, size, lc); + } catch(NotEnoughSpaceException e) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + } + return; + } + + public void processActivateFile(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + if (p1 != 0x00 || p2 != 0x00) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + // only a file or the applet can be selected + File file = getCurrentlySelectedEF(); + if (file == null) { + file = getCurrentlySelectedDF(); + if (file != this) { + ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); + } + } + + authenticateAction(file, ACL_OP_EF_ACTIVATE); + + file.setState(STATE_OPERATIONAL_ACTIVATED); + + if (file == this) { + pinManager.SetInitializationMode(false); + } + + } + +// TODO If file lifecycles are to be implemented: +// - DEACTIVATE FILE 04 +// - ACTIVATE FILE 44 +// - TERMINATE DF E6 +// - TERMINATE EF E8 +} diff --git a/src/com/mysmartlogon/gidsApplet/GidsPIN.java b/src/com/mysmartlogon/gidsApplet/GidsPIN.java new file mode 100644 index 0000000..a9cf22d --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/GidsPIN.java @@ -0,0 +1,62 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.OwnerPIN; + +/** + * \brief The IsoPIN class. + * + * Allows the PIN to be deauthenticated + */ + +public class GidsPIN extends OwnerPIN { + + + private byte currentPINLen = 0; + private byte minPINSize = 0; + private byte maxPINSize = 0; + private byte tryLimit = 0; + + public GidsPIN(byte tryLimit, byte maxPINSize, byte minPINSize) { + super(tryLimit, maxPINSize); + this.maxPINSize = maxPINSize; + this.tryLimit = tryLimit; + this.minPINSize = minPINSize; + } + + public byte GetCurrentPINLen() { + return currentPINLen; + } + + public byte GetMinPINSize() { + return minPINSize; + } + + public byte GetMaxPINSize() { + return maxPINSize; + } + + public void CheckLength(byte len) { + if (len < minPINSize || len > maxPINSize) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + } + + public void setAsAuthenticated() { + this.setValidatedFlag(true); + } + + @Override + public void update(byte[] pin, + short offset, + byte length) { + super.update(pin, offset, length); + currentPINLen = length; + } + + public byte getTryLimit() { + return tryLimit; + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/GidsPINManager.java b/src/com/mysmartlogon/gidsApplet/GidsPINManager.java new file mode 100644 index 0000000..aa4d09f --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/GidsPINManager.java @@ -0,0 +1,569 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.APDU; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.JCSystem; +import javacard.framework.Util; +import javacard.security.DESKey; +import javacard.security.KeyBuilder; +import javacard.security.RandomData; +import javacardx.crypto.Cipher; + +public class GidsPINManager { + + /* PIN, PUK and key realted constants */ + // PIN: + private static final byte PIN_MAX_TRIES = 3; + private static final byte PIN_MIN_LENGTH = 4; + private static final byte PIN_MAX_LENGTH = 16; + + private static final byte EXTERNAL_CHALLENGE = 1; + private static final byte MUTUAL_CHALLENGE = 2; + private static final byte EXTERNAL_AUTHENTICATED = 3; + private static final byte MUTUAL_AUTHENTICATED = 4; + + + private GidsPIN pin_pin = null; + + private boolean isInInitializationMode = true; + + private byte[] ExternalChallenge = null; + private byte[] CardChallenge = null; + private Object[] KeyReference = null; + private byte[] buffer = null; + private byte[] sharedKey = null; + private byte[] status = null; + + public GidsPINManager() { + pin_pin = new GidsPIN(PIN_MAX_TRIES, PIN_MAX_LENGTH, PIN_MIN_LENGTH); + ExternalChallenge = JCSystem.makeTransientByteArray((short)16, JCSystem.CLEAR_ON_DESELECT); + CardChallenge = JCSystem.makeTransientByteArray((short)16, JCSystem.CLEAR_ON_DESELECT); + KeyReference = JCSystem.makeTransientObjectArray((short)1, JCSystem.CLEAR_ON_DESELECT); + buffer = JCSystem.makeTransientByteArray((short)40, JCSystem.CLEAR_ON_DESELECT); + sharedKey = JCSystem.makeTransientByteArray((short)40, JCSystem.CLEAR_ON_DESELECT); + status = JCSystem.makeTransientByteArray((short)1, JCSystem.CLEAR_ON_DESELECT); + } + + private GidsPIN GetPINByReference(byte reference) throws NotFoundException { + switch(reference) { + case (byte) 0x80: + case (byte) 0x00: + return pin_pin; + case (byte) 0x81: + //return pin_puk; + default: + throw NotFoundException.getInstance(); + } + } + + public void SetInitializationMode(boolean value) { + isInInitializationMode = value; + if (value == false) { + DeauthenticateAllPin(); + } + } + + public void DeauthenticateAllPin() { + pin_pin.reset(); + // deauthenticate admin key + status[0] = 0; + ClearChallengeData(); + // clear shared key + Util.arrayFillNonAtomic(sharedKey, (short) 0, (short) sharedKey.length, (byte)0x00); + KeyReference[0] = null; + } + + private boolean CheckUserAuthentication() { + if (!isInInitializationMode) { + if (!pin_pin.isValidated()) { + return false; + } + } + return true; + } + + private boolean CheckExternalOrMutualAuthentication() { + if (!isInInitializationMode) { + if (status[0] != EXTERNAL_AUTHENTICATED && status[0] != MUTUAL_AUTHENTICATED) { + return false; + } + } + return true; + } + + public void SetKeyReference(CRTKeyFile crt) { + KeyReference[0] = crt; + } + + + /** + * \brief throw a SW_SECURITY_STATUS_NOT_SATISFIED exception if not allowed + */ + public void CheckACL(byte acl) { + if(acl == (byte) 0x00) { // No restrictions. + return; + } else if(acl == (byte) 0xFF) { // Never. + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + byte SEID = (byte)(acl & (byte)0x0F); + // contact / contact less ACL + if (SEID > 0) { + byte protocol = (byte) (APDU.getProtocol() & APDU.PROTOCOL_MEDIA_MASK); + if (SEID == 1) { + // contact operation + if (protocol != APDU.PROTOCOL_MEDIA_USB && protocol != APDU.PROTOCOL_MEDIA_DEFAULT) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } else if (SEID == 2) { + // contact less operation + if (protocol != APDU.PROTOCOL_MEDIA_CONTACTLESS_TYPE_A && protocol != APDU.PROTOCOL_MEDIA_CONTACTLESS_TYPE_B) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } + } + byte authentication = (byte)(acl & (byte)0xF0); + if(authentication == (byte) 0x90) { + // PIN required. + if (CheckUserAuthentication()) { + return; + } + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + if ((byte)(authentication&(byte)0x90) == (byte)0x10) { + // PIN can valid the ACL + if (CheckUserAuthentication()) { + return; + } + // else continue + } + if(authentication == (byte) 0xA0) { + // external / mutal authentication mandatory + if (CheckExternalOrMutualAuthentication()) { + return; + } + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + if((authentication&(byte)0xA0) == (byte)0x20) { + // external or mutal authentication optional + if (CheckExternalOrMutualAuthentication()) { + return; + } + // else continue + } + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + + /** + * \brief Process the VERIFY apdu (INS = 20). + * + * This apdu is used to verify a PIN and authenticate the user. A counter is used + * to limit unsuccessful tries (i.e. brute force attacks). + * + * \param apdu The apdu. + * + * \throw ISOException SW_INCORRECT_P1P2, ISO7816.SW_WRONG_LENGTH, SW_PIN_TRIES_REMAINING. + */ + public void processVerify(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + short offset_cdata; + short lc; + GidsPIN pin = null; + + // P1P2 0001 only at the moment. (key-reference 01 = PIN) + if(buf[ISO7816.OFFSET_P1] != 0x00) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + if (buf[ISO7816.OFFSET_P2] == (byte) 0x82) { + // special resetting code for GIDS + DeauthenticateAllPin(); + return; + } + + try { + pin = GetPINByReference(buf[ISO7816.OFFSET_P2]); + } + catch(NotFoundException e) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + if (pin.getTriesRemaining() == (byte) 0) { + // pin blocked + ISOException.throwIt(ISO7816.SW_FILE_INVALID); + } + + // Lc might be 0, in this case the caller checks if verification is required. + if((lc > 0 && (lc < pin.GetMinPINSize()) || lc > pin.GetMaxPINSize())) { + ISOException.throwIt((short) (ErrorCode.SW_PIN_TRIES_REMAINING | pin.getTriesRemaining())); + } + + // Caller asks if verification is needed. + if(lc == 0) { + if (!isInInitializationMode) { + // Verification required, return remaining tries. + ISOException.throwIt((short)(ErrorCode.SW_PIN_TRIES_REMAINING | pin.getTriesRemaining())); + } else { + // No verification required. + ISOException.throwIt(ISO7816.SW_NO_ERROR); + } + } + + // Check the PIN. + if(!pin.check(buf, offset_cdata, (byte) lc)) { + ISOException.throwIt((short)(ErrorCode.SW_PIN_TRIES_REMAINING | pin.getTriesRemaining())); + } else { + + } + } + + /** + * \brief Process the CHANGE REFERENCE DATA apdu (INS = 24). + * + * If the state is STATE_CREATION, we can set the PUK without verification. + * The state will advance to STATE_INITIALISATION (i.e. the PUK must be set before the PIN). + * In a "later" state the user must authenticate himself to be able to change the PIN. + * + * \param apdu The apdu. + * + * \throws ISOException SW_INCORRECT_P1P2, ISO7816.SW_WRONG_LENGTH, SW_PIN_TRIES_REMAINING. + */ + public void processChangeReferenceData(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short offset_cdata; + GidsPIN pin = null; + + // Buffer verification. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + if (p1 == (byte) 0x01) { + try { + pin = GetPINByReference(p2); + } + catch(NotFoundException e) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + + // Check length. + pin.CheckLength((byte) lc); + + // authentication not needed for the first pin set + if(!isInInitializationMode) { + if (!pin.isValidated()) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } + // Set PIN value + pin.update(buf, offset_cdata, (byte)lc); + if(isInInitializationMode) { + pin.resetAndUnblock(); + } + + } else if (p1 == (byte) 0x00) { + try { + pin = GetPINByReference(buf[ISO7816.OFFSET_P2]); + } + catch(NotFoundException e) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + + // Check PIN lengths + if(lc > (short)(pin.GetMaxPINSize() *2) || lc < (short)(pin.GetMinPINSize() *2)) { + ISOException.throwIt((short) (ErrorCode.SW_PIN_TRIES_REMAINING | pin.getTriesRemaining())); + } + + byte currentPinLength = pin.GetCurrentPINLen(); + // if the current pin is very long and the tested pin is very short, force the verification to decreate the remaining try count + // do not allow the revelation of currentPinLength until pin.check is done + if (lc < currentPinLength) { + currentPinLength = (byte) lc; + } + if (pin.getTriesRemaining() == (byte) 0) { + // pin blocked + ISOException.throwIt(ISO7816.SW_FILE_INVALID); + } + // Check the old PIN. + if(!pin.check(buf, offset_cdata, currentPinLength)) { + ISOException.throwIt((short)(ErrorCode.SW_PIN_TRIES_REMAINING | pin.getTriesRemaining())); + } + if(lc > (short)(pin.GetMaxPINSize() + currentPinLength) || lc < (short)(currentPinLength + pin.GetMinPINSize())) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + // UPDATE PIN + pin.update(buf, (short) (offset_cdata+currentPinLength), (byte) (lc - currentPinLength)); + pin.setAsAuthenticated(); + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + }// end processChangeReferenceData() + + + + + /** + * \brief Process the RESET RETRY COUNTER apdu (INS = 2C). + * + * This is used to unblock the PIN with the PUK and set a new PIN value. + * + * \param apdu The RESET RETRY COUNTER apdu. + * + * \throw ISOException SW_COMMAND_NOT_ALLOWED, ISO7816.SW_WRONG_LENGTH, SW_INCORRECT_P1P2, + * SW_PIN_TRIES_REMAINING. + */ + public void processResetRetryCounter(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short offset_cdata; + GidsPIN pin = null; + + if(isInInitializationMode) { + ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED); + } + if(p1 == (byte) 0x02) { + // this suppose a previous authentication of the admin via + // external or mutual authenticate + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + // only P2 = 80 is specified + if (p2 != (byte) 0x80) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + try { + pin = GetPINByReference(p2); + } + catch(NotFoundException e) { + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + } + if (!CheckExternalOrMutualAuthentication()) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + // Check length. + pin.CheckLength((byte) lc); + // Set PIN value + pin.update(buf, offset_cdata, (byte)lc); + pin.resetAndUnblock(); + // admin is deauthenticated at the end of the process + DeauthenticateAllPin(); + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + } + + public void processGeneralAuthenticate(APDU apdu) { + byte[] buf = apdu.getBuffer(); + byte p1 = buf[ISO7816.OFFSET_P1]; + byte p2 = buf[ISO7816.OFFSET_P2]; + short lc; + short offset_cdata; + + if(isInInitializationMode) { + ISOException.throwIt(ISO7816.SW_COMMAND_NOT_ALLOWED); + } + + if(p1 != (byte) 0x00 || p2 != (byte) 0x00 ) { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + } + + // Bytes received must be Lc. + lc = apdu.setIncomingAndReceive(); + if(lc != apdu.getIncomingLength()) { + ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); + } + offset_cdata = apdu.getOffsetCdata(); + + short innerPos = 0, innerLen = 0; + if (buf[offset_cdata] != (byte) 0x7C) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + + try { + innerLen = UtilTLV.decodeLengthField(buf, (short) (offset_cdata+1)); + innerPos = (short) (offset_cdata + 1 + UtilTLV.getLengthFieldLength(buf, (short) (offset_cdata+1))); + } catch (InvalidArgumentsException e1) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + // inner functions never return if their input tag is found + if (CheckForExternalChallenge(apdu, buf, innerPos, innerLen)) { + return; + } + if (CheckForChallengeResponse(apdu, buf, innerPos, innerLen)) { + return; + } + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + + private void ClearChallengeData() { + Util.arrayFillNonAtomic(ExternalChallenge, (short) 0, (short) ExternalChallenge.length, (byte)0x00); + Util.arrayFillNonAtomic(CardChallenge, (short) 0, (short) CardChallenge.length, (byte)0x00); + Util.arrayFillNonAtomic(buffer, (short) 0, (short) buffer.length, (byte)0x00); + Util.arrayFillNonAtomic(status, (short) 0, (short) status.length, (byte)0x00); + } + + private boolean CheckForExternalChallenge(APDU apdu, byte[] buf, short innerPos, short innerLen) { + short pos = 0, len = 0; + try { + pos = UtilTLV.findTag(buf, innerPos, innerLen, (byte) 0x81); + if (buf[(short) (pos+1)] == 0) { + // zero len TLV allowed + len = 0; + } else { + len = UtilTLV.decodeLengthField(buf, (short)(pos+1)); + } + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (NotFoundException e) { + return false; + } + ClearChallengeData(); + + pos += 1 + UtilTLV.getLengthFieldLength(buf, (short)(pos+1)); + // challenge size = 16 => mutual authentication + // challenge size = 0 => external authentication, request for a challenge + if (len == (short)16) { + Util.arrayCopyNonAtomic(buf, pos, ExternalChallenge, (short) 0, len); + // generate a 16 bytes challenge + status[0] = MUTUAL_CHALLENGE; + } else if (len == 0) { + // generate a 8 bytes challenge + len = 8; + status[0] = EXTERNAL_CHALLENGE; + } else { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + RandomData randomData = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + randomData.generateData(CardChallenge, (short) 0, len); + + pos = 0; + buf[pos++] = (byte) 0x7C; + buf[pos++] = (byte) (len + 2); + buf[pos++] = (byte) 0x81; + buf[pos++] = (byte) (len); + Util.arrayCopyNonAtomic(CardChallenge, (short) 0, buf, pos, len); + apdu.setOutgoingAndSend((short)0, (short) (len + 4)); + return true; + } + + private boolean CheckForChallengeResponse(APDU apdu, byte[] buf, short innerPos, short innerLen) { + short pos = 0, len = 0; + try { + pos = UtilTLV.findTag(buf, innerPos, innerLen, (byte) 0x82); + len = UtilTLV.decodeLengthField(buf, (short)(pos+1)); + } catch (InvalidArgumentsException e) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } catch (NotFoundException e) { + return false; + } + + pos += 1 + UtilTLV.getLengthFieldLength(buf, (short)(pos+1)); + if (len > (short)40) { + ISOException.throwIt(ISO7816.SW_DATA_INVALID); + } + if (status[0] == MUTUAL_CHALLENGE) { + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey key = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + key.setKey(((CRTKeyFile)(KeyReference[0])).GetSymectricKey(), (short) 0); + + //decrypt message + cipherDES.init(key, Cipher.MODE_DECRYPT); + cipherDES.doFinal(buf, pos, len, buffer, (short) 0); + + if (Util.arrayCompare(buffer, (short) 0, CardChallenge, (short) 0, (short) 16) != 0) { + ClearChallengeData(); + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + if (Util.arrayCompare(buffer, (short) 16, ExternalChallenge, (short) 0, (short) 16) != 0) { + ClearChallengeData(); + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + Util.arrayCopy(buffer, (short) 32, sharedKey, (short) 0, (short) (len - 32)); + + RandomData randomData = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + randomData.generateData(sharedKey, (short) (len - 32), (short) (len - 32)); + + Util.arrayCopy(buffer, (short) 32, sharedKey, (short) (len - 32), (short) (len - 32)); + + cipherDES.init(key, Cipher.MODE_ENCRYPT); + cipherDES.doFinal(buffer, (short) 0, len, buf, (short) 0); + + // avoid replay attack + ClearChallengeData(); + status[0] = MUTUAL_AUTHENTICATED; + + apdu.setOutgoing(); + apdu.setOutgoingLength(len); + apdu.sendBytes((short) 0, len); + } else if (status[0] == EXTERNAL_CHALLENGE) { + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey key = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + key.setKey(((CRTKeyFile)(KeyReference[0])).GetSymectricKey(), (short) 0); + + //decrypt message + cipherDES.init(key, Cipher.MODE_DECRYPT); + cipherDES.doFinal(buf, pos, len, buffer, (short) 0); + + if (Util.arrayCompare(buffer, (short) 0, CardChallenge, (short) 0, (short) 8) != 0) { + ClearChallengeData(); + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + + // avoid replay attack + ClearChallengeData(); + status[0] = EXTERNAL_AUTHENTICATED; + } else { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + return true; + } + + public void returnPINStatus(APDU apdu, short id) { + byte[] buf = apdu.getBuffer(); + GidsPIN pin = null; + switch(id) { + default: + ISOException.throwIt(ErrorCode.SW_REFERENCE_DATA_NOT_FOUND); + break; + case (short) 0x7F71: + case (short) 0x7F72: + pin = pin_pin; + break; + } + + Util.setShort(buf, (short) 0, id); + buf[2] = (byte) 0x06; + buf[3] = (byte) 0x97; + buf[4] = (byte) 0x01; + buf[5] = pin.getTriesRemaining(); + buf[6] = (byte) 0x93; + buf[7] = (byte) 0x01; + buf[8] = pin.getTryLimit(); + apdu.setOutgoing(); + apdu.setOutgoingLength((short)9); + apdu.sendBytes((short) 0, (short) 9); + + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/InvalidArgumentsException.java b/src/com/mysmartlogon/gidsApplet/InvalidArgumentsException.java new file mode 100644 index 0000000..f0c420b --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/InvalidArgumentsException.java @@ -0,0 +1,53 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +/** + * \brief The InvalidArgumentsException class. + * + * \attention This singleton is not thread-safe. + */ +@SuppressWarnings("serial") +public class InvalidArgumentsException extends Exception { + /** + * + */ + public static InvalidArgumentsException instance; + + /** + * \brief Private access constructor (Singleton pattern). + */ + private InvalidArgumentsException() { + + } + + /** + * \brief Get the instance. + * + * \return The InvalidArgumentsException instance. + */ + public static InvalidArgumentsException getInstance() { + if(instance == null) { + instance = new InvalidArgumentsException(); + } + return instance; + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/NotEnoughSpaceException.java b/src/com/mysmartlogon/gidsApplet/NotEnoughSpaceException.java new file mode 100644 index 0000000..eafb426 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/NotEnoughSpaceException.java @@ -0,0 +1,59 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +/** + * \brief Exception class. + * + * This class is a singleton in order to save resources. + * Should be thrown whenever: + * - A limit for the maximum amount of children of a DedicatedFile is exceeded. + * - An array can not hold that amount of data. + * - Memory allocation failed due to full RAM/EEPROM. + * + * \attention This singleton is not thread-safe. + */ +@SuppressWarnings("serial") +public class NotEnoughSpaceException extends Exception { + /** + * + */ + public static NotEnoughSpaceException instance; + + + /** + * \brief Private access constructor (Singleton pattern). + */ + private NotEnoughSpaceException() { + + } + + /** + * \brief Get the instance. + * + * \return The FileNotFoundException instance. + */ + public static NotEnoughSpaceException getInstance() { + if(instance == null) { + instance = new NotEnoughSpaceException(); + } + return instance; + } +} diff --git a/src/com/mysmartlogon/gidsApplet/NotFoundException.java b/src/com/mysmartlogon/gidsApplet/NotFoundException.java new file mode 100644 index 0000000..3ce9d02 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/NotFoundException.java @@ -0,0 +1,56 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +/** + * \brief The NotFoundException class. + * + * Should be thrown whenever a specified file or tag in a TLV structure + * could not be found. This class is a singleton in order to save resources. + * + * \attention This singleton is not thread-safe. + */ +@SuppressWarnings("serial") +public class NotFoundException extends Exception { + /** + * + */ + public static NotFoundException instance; + + /** + * \brief Private access constructor (Singleton pattern). + */ + private NotFoundException() { + + } + + /** + * \brief Get the instance. + * + * \return The NotFoundException instance. + */ + public static NotFoundException getInstance() { + if(instance == null) { + instance = new NotFoundException(); + } + return instance; + } + +} diff --git a/src/com/mysmartlogon/gidsApplet/Record.java b/src/com/mysmartlogon/gidsApplet/Record.java new file mode 100644 index 0000000..ecfaa44 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/Record.java @@ -0,0 +1,60 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.Util; + +/** + * \brief A Record. + * + * This class is necessary because multidimensional arrays are not supported by the JCVM. + */ +public class Record { + byte[] data; + + /** + * \brief Constructor. + * + * \param data The byte array to store. No copy is made. + */ + Record(byte[] data) { + this.data = data; + } + + /** + * \brief Constructor. + * + * A new byte array is being allocated. Use the data-field to fill it up with data. + * + * \param size The size of the data array. + */ + Record(short size) { + this.data = new byte[size]; + } + + byte[] GetData() { + return this.data; + } + + public void clearContents() { + Util.arrayFillNonAtomic(data, (short)0, (short)data.length, (byte)0); + } +} + diff --git a/src/com/mysmartlogon/gidsApplet/TransmitManager.java b/src/com/mysmartlogon/gidsApplet/TransmitManager.java new file mode 100644 index 0000000..51eda83 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/TransmitManager.java @@ -0,0 +1,292 @@ +package com.mysmartlogon.gidsApplet; + +import javacard.framework.APDU; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.JCSystem; +import javacard.framework.Util; + +public class TransmitManager { + + // a ram buffer for public key export (no need to allocate flash !) + private static final short RAM_BUF_SIZE = (short) 1220; + private byte[] ram_buf = null; + // internal variables to do chaining + private short[] chaining_cache = null; + // store special object to returns or if null, use the ram buffer + private Object[] chaining_object = null; + + // number of variables for the cache + private static final short CHAINING_CACHE_SIZE = (short) 6; + // index of the object (when sending Record[]) + private static final short CHAINING_OBJECT_INDEX = (short) 0; + // current offset + private static final short RAM_CHAINING_CACHE_OFFSET_CURRENT_POS = (short) 1; + // max size (if ram buffer) + private static final short RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING = (short) 2; + // previous APDU data to check consistancy between chain + private static final short RAM_CHAINING_CACHE_OFFSET_CURRENT_INS = (short) 3; + private static final short RAM_CHAINING_CACHE_OFFSET_CURRENT_P1P2 = (short) 4; + private static final short RAM_CHAINING_CACHE_PUT_DATA_OFFSET = (short) 5; + // index of the object array + private static final short CHAINING_OBJECT = (short) 0; + private static final short PUT_DATA_OBJECT = (short) 1; + + public TransmitManager() { + ram_buf = JCSystem.makeTransientByteArray(RAM_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT); + chaining_cache = JCSystem.makeTransientShortArray(CHAINING_CACHE_SIZE, JCSystem.CLEAR_ON_DESELECT); + chaining_object = JCSystem.makeTransientObjectArray((short) 2, JCSystem.CLEAR_ON_DESELECT); + + } + + private void Clear(boolean buffer) { + if (buffer) { + Util.arrayFillNonAtomic(ram_buf, (short)0, RAM_BUF_SIZE, (byte)0x00); + } + chaining_cache[CHAINING_OBJECT_INDEX] = 0; + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS] = 0; + chaining_cache[RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING] = 0; + chaining_cache[RAM_CHAINING_CACHE_PUT_DATA_OFFSET] = 0; + chaining_object[CHAINING_OBJECT] = null; + chaining_object[PUT_DATA_OBJECT] = null; + } + + public byte[] GetRamBuffer() { + return ram_buf; + } + + public void ClearRamBuffer() { + Clear(true); + } + + /** + * \brief Parse the apdu's CLA byte to determine if the apdu is the first or second-last part of a chain. + * + * The Java Card API version 2.2.2 has a similar method (APDU.isCommandChainingCLA()), but tests have shown + * that some smartcard platform's implementations are wrong (not according to the JC API specification), + * specifically, but not limited to, JCOP 2.4.1 R3. + * + * \param apdu The apdu. + * + * \return true If the apdu is the [1;last[ part of a command chain, + * false if there is no chain or the apdu is the last part of the chain. + */ + static boolean isCommandChainingCLA(APDU apdu) { + byte[] buf = apdu.getBuffer(); + return ((byte)(buf[0] & (byte)0x10) == (byte)0x10); + } + + public void processChainInitialization(APDU apdu) { + byte buffer[] = apdu.getBuffer(); + byte ins = buffer[ISO7816.OFFSET_INS]; + // Command chaining checks & initialization + if(chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_INS] != 0 || isCommandChainingCLA(apdu)) { + short p1p2 = Util.getShort(buffer, ISO7816.OFFSET_P1); + /* + * Command chaining only for: + * - PERFORM SECURITY OPERATION + * - GENERATE ASYMMETRIC KEYKAIR + * - PUT DATA + * when not using extended APDUs. + */ + if( (ins != GidsApplet.INS_PERFORM_SECURITY_OPERATION + && ins != GidsApplet.INS_GENERATE_ASYMMETRIC_KEYPAIR + && ins != GidsApplet.INS_PUT_DATA)) { + ISOException.throwIt(ISO7816.SW_COMMAND_CHAINING_NOT_SUPPORTED); + } + + if(chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_INS] == 0 + && chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_P1P2] == 0) { + /* A new chain is starting - set the current INS and P1P2. */ + if(ins == 0) { + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_INS] = ins; + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_P1P2] = p1p2; + } else if(chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_INS] != ins + || chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_P1P2] != p1p2) { + /* The current chain is not yet completed, + * but an apdu not part of the chain had been received. */ + ISOException.throwIt(ErrorCode.SW_COMMAND_NOT_ALLOWED_GENERAL); + } else if(!isCommandChainingCLA(apdu)) { + /* A chain is ending, set the current INS and P1P2 to zero to indicate that. */ + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_INS] = 0; + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_P1P2] = 0; + } + } + + // If the card expects a GET RESPONSE, no other operation should be requested. + if(chaining_cache[RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING] > 0 && ins != GidsApplet.INS_GET_RESPONSE) { + // clear the buffer + Clear(true); + } + if (ins != GidsApplet.INS_PUT_DATA) { + clearCachedRecord(); + } + } + + /** + * \brief Receive the data sent by chaining or extended apdus and store it in ram_buf. + * + * This is a convienience method if large data has to be accumulated using command chaining + * or extended apdus. The apdu must be in the INITIAL state, i.e. setIncomingAndReceive() + * might not have been called already. + * + * \param apdu The apdu object in the initial state. + * + * \throw ISOException SW_WRONG_LENGTH + */ + public short doChainingOrExtAPDU(APDU apdu) throws ISOException { + byte[] buf = apdu.getBuffer(); + short recvLen = apdu.setIncomingAndReceive(); + short offset_cdata = apdu.getOffsetCdata(); + + // Receive data (short or extended). + while (recvLen > 0) { + if((short)(chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS] + recvLen) > RAM_BUF_SIZE) { + ISOException.throwIt(ISO7816.SW_FILE_FULL); + } + Util.arrayCopyNonAtomic(buf, offset_cdata, ram_buf, chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS], recvLen); + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS] += recvLen; + recvLen = apdu.receiveBytes(offset_cdata); + } + + if(isCommandChainingCLA(apdu)) { + // We are still in the middle of a chain, otherwise there would not have been a chaining CLA. + // Make sure the caller does not forget to return as the data should only be interpreted + // when the chain is completed (when using this method). + ISOException.throwIt(ISO7816.SW_NO_ERROR); + return (short)0; + } else { + // Chain has ended or no chaining. + // We did receive the data, everything is fine. + // Reset the current position in ram_buf. + recvLen = (short) (recvLen + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS]); + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS] = 0; + return recvLen; + } + } + + /** + * \brief Process the GET RESPONSE APDU (INS=C0). + * + * If there is content available in ram_buf that could not be sent in the last operation, + * the host should use this APDU to get the data. The data is cached in ram_buf. + * + * \param apdu The GET RESPONSE apdu. + * + * \throw ISOException SW_CONDITIONS_NOT_SATISFIED, SW_UNKNOWN, SW_CORRECT_LENGTH. + */ + public void processGetResponse(APDU apdu) { + sendData(apdu); + } + + /** + * \brief Send the data from ram_buf, using either extended APDUs or GET RESPONSE. + * + * \param apdu The APDU object, in STATE_OUTGOING state. + * + * \param pos The position in ram_buf at where the data begins + * + * \param len The length of the data to be sent. If zero, 9000 will be + * returned + */ + private void sendData(APDU apdu) { + short le; + short remaininglen = 0; + byte data[] = null; + short pos = chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS]; + if (chaining_object[CHAINING_OBJECT] == null) { + data = ram_buf; + remaininglen = chaining_cache[RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING]; + } else if (chaining_object[CHAINING_OBJECT] instanceof Record) { + Record record = (Record) (chaining_object[CHAINING_OBJECT]); + data = record.GetData(); + remaininglen = (short) (((short) data.length) - pos); + } /* else if (chaining_object[CHAINING_OBJECT] instanceof Record[]) { + short index = chaining_cache[CHAINING_OBJECT_INDEX]; + Record[] records = (Record[]) (chaining_object[CHAINING_OBJECT]); + data = records[index].GetData(); + remaininglen = (short) (((short) data.length) - pos); + //TODO adjust with next object + }*/ + // no buffer requested ? end the transfert + + le = apdu.setOutgoing(); + if(le <= 0) { + Clear(true); + return; + //ISOException.throwIt(ISO7816.SW_NO_ERROR); + } + + + // We have 256 Bytes send-capacity per APDU. + short sendLen = remaininglen > le ? le : remaininglen; + apdu.setOutgoingLength(sendLen); + /*if (chaining_object[CHAINING_OBJECT] instanceof Record[]) { + apdu.sendBytesLong(data, pos, sendLen); + } else {*/ + apdu.sendBytesLong(data, pos, sendLen); + //} + + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS]+= sendLen; + + if (chaining_object[CHAINING_OBJECT] == null) { + chaining_cache[RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING] -= sendLen; + } + remaininglen -= sendLen; + if(remaininglen > 0) { + short nextRespLen = remaininglen > 256 ? 256 : remaininglen; + ISOException.throwIt( (short)(ISO7816.SW_BYTES_REMAINING_00 | nextRespLen) ); + } else { + Clear(true); + return; //ISOException.throwIt(ISO7816.SW_NO_ERROR); + } + } + + public void sendRecord(APDU apdu, Record data) { + Clear(true); + chaining_object[CHAINING_OBJECT] = data; + sendData(apdu); + } + + public void sendRecords(APDU apdu, Record[] data) { + Clear(true); + chaining_object[CHAINING_OBJECT] = data; + sendData(apdu); + } + + public void sendDataFromRamBuffer(APDU apdu, short offset, short length) { + Clear(false); + chaining_cache[RAM_CHAINING_CACHE_OFFSET_CURRENT_POS] = offset; + chaining_cache[RAM_CHAINING_CACHE_OFFSET_BYTES_REMAINING] = length; + sendData(apdu); + } + + /* functions used to cache a Record object for chained PUT DATA. + * We cannot use the ram buffer because it is too small. */ + public Record returnCachedRecord() { + Object object = chaining_object[PUT_DATA_OBJECT]; + if (object != null && object instanceof Record) { + return (Record) object; + } + return null; + } + + public void setCachedRecord(Record record) { + chaining_object[PUT_DATA_OBJECT] = record; + } + + public short returnCachedOffset() { + return chaining_cache[RAM_CHAINING_CACHE_PUT_DATA_OFFSET]; + } + + public void setCachedOffset(short offset) { + chaining_cache[RAM_CHAINING_CACHE_PUT_DATA_OFFSET] = offset; + } + + public void clearCachedRecord() { + chaining_object[PUT_DATA_OBJECT] = null; + chaining_cache[RAM_CHAINING_CACHE_PUT_DATA_OFFSET] = 0; + } +} diff --git a/src/com/mysmartlogon/gidsApplet/UtilTLV.java b/src/com/mysmartlogon/gidsApplet/UtilTLV.java new file mode 100644 index 0000000..6f913e3 --- /dev/null +++ b/src/com/mysmartlogon/gidsApplet/UtilTLV.java @@ -0,0 +1,298 @@ +/* + * IsoApplet: A Java Card PKI applet aimiing for ISO 7816 compliance. + * Copyright (C) 2014 Philip Wendland (wendlandphilip@gmail.com) + * + * 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 com.mysmartlogon.gidsApplet; + +import javacard.framework.Util; + +/** + * \brief Utility class for TLV-realted operations. + */ +public class UtilTLV { + + /** \brief Find the position of the tag at level 1. + * + * \attention This method only searches level 1 of TLV encoded arrays (i.e. no nested TLVs are searched). + * + * \param tlv The array containing the TLV-encoded object to search. + * + * \param tlvOffset The position at which the TLV structure begins. + * + * \param tlvLength The length of the TLV structure. + * + * \param tag The tag to search for. + * + * \return The position of the tag. + * + * \throw NotFoundException If the tag could not be found. + * + * \throw InvalidArgumentsException Malformatted TLV data. + */ + public static short findTag(byte[] tlv, short tlvOffset, short tlvLength, byte tag) throws NotFoundException, InvalidArgumentsException { + short tagPos = tlvOffset; + short len; + + while(tagPos < (short)(tlvLength+tlvOffset-1)) { + if(tlv[tagPos] == tag) { + return tagPos; + } + len = decodeLengthField(tlv, (short)(tagPos+1)); + // Increase the position by: T length (1), L length, V length. + // I.e. look at the next Tag, jump over current L and V field. + // This saves execution time and ensures that no byte from V is misinterpreted. + tagPos += 1 + getLengthFieldLength(tlv, (short)(tagPos+1)) + len; + } + throw NotFoundException.getInstance(); + } + + /** + * \brief Check the consistency of the TLV structure. + * + * Basically, we jump from one tag to the next. At the end, we must be at the position + * where the next tag would be, if it was there. If the position is any other than that, + * the TLV structure is not consistent. + * + * \param tlv The array containing the TLV-encoded object to search. + * + * \param offset The position at which the TLV structure begins. + * + * \param length The length of the TLV structure. + * + * \return True if the TLV structure is valid, else false. + */ + public static boolean isTLVconsistent(byte[] tlv, short offset, short length) { + short pos = offset; + short len; + + while(pos < (short)(length+offset-1)) { + try { + len = decodeLengthField(tlv, (short)(pos+1)); + pos += 1 + getLengthFieldLength(tlv, (short)(pos+1)) + len; + } catch (InvalidArgumentsException e) { + return false; + } + } + return (pos == (short)(offset+length)); + } + + /** + * \brief Decode the length field of a TLV-entry. + * + * The length field itself can be 1, 2 or 3 bytes long: + * - If the length is between 0 and 127, it is 1 byte long. + * - If the length is between 128 and 255, it is 2 bytes long. + * The first byte is 0x81 to indicate this. + * - If the length is between 256 and 65535, it is 3 bytes long. + * The first byte is 0x82, the following 2 contain the actual length. + * Note: Only lengths up to 0x7FFF (32767) are supported here, because a short in Java is signed. + * + * \param buf The buffer containing the length field. + * + * \param offset The offset at where the length field starts. + * + * \param length The length of the buffer (buf). This is to prevent that the index gets out of bounds. + * + * \return The (positive) length encoded by the length field, or in case of an error, -1. + * + * \throw InvalidArgumentsException If offset is too big for a signed Java short + * If the first byte of the length field is invalid + */ + public static short decodeLengthField(byte[] buf, short offset) throws InvalidArgumentsException { + if(buf[offset] == (byte)0x82) { // 256..65535 + // Check for short overflow + // (In Java, a short is signed: positive values are 0000..7FFF) + if(buf[(short)(offset+1)] < 0) { // 80..FF + throw InvalidArgumentsException.getInstance(); + } + return Util.getShort(buf, (short)(offset+1)); + } else if(buf[offset] == (byte)0x81) { + return (short) ( 0x00FF & buf[(short)(offset+1)]); + } else if(buf[offset] > 0) { // 00..7F + return (short) ( 0x007F & buf[offset]); + } else { + throw InvalidArgumentsException.getInstance(); + } + } + + /** + * \brief Get the length of the length field of a TLV-entry. + * + * \attention Not the length of the value-field is returned, + * but the length of the length field itself. + * + * \see decodeLengthField() + * + * \param length The decoded length from the TLV-entry. + * + * \return The length of the length field. + * + * \throw InvalidArgumentsException If the length would overflow the signed + * short of Java. + */ + public static short getLengthFieldLength(byte[] buf, short offset) { + if(buf[offset] == (byte)0x82) { // 256..65535 + // Check for short overflow + // (In Java, a short is signed: positive values are 0000..7FFF) + return (short)3; + } else if(buf[offset] == (byte)0x81) { + return (short)2; + } else { + return (short)1; + } + } + + /** + * \brief Get the length of the length field of a TLV-entry. + * + * \attention Not the length of the value-field is returned, + * but the length of the length field itself. + * + * \see decodeLengthField() + * + * \param length The decoded length from the TLV-entry. + * + * \return The length of the length field. + * + * \throw InvalidArgumentsException If the length would overflow the signed + * short of Java. + */ + public static short getEncodingLengthFieldLength(short length) throws InvalidArgumentsException { + if(length < 0) { + throw InvalidArgumentsException.getInstance(); + } else if(length < 128) { + return 1; + } else if(length < 256) { + return 2; + } else { + return 3; + } + } + + /* returns 0 or the size of the BERTLV structure */ + public static short CheckBERTLV(byte[] buf, short offset, short len) { + short size = 0; + short sizeforsize = 0; + short i = 1; + short totalsize = 0; + + if ((buf[offset] & 0x1F) != 0x1F) { + // simplified tag + } else { + // tag start with all 5 last bits to 1 + // skip the tag + while (((buf[(short) (offset + i)] & 0x80) != 0) && i < len) { + i++; + } + // pass the last byte of the tag + i+=1; + } + if ((short) (i+1) >len) { + return 0; + } + // check the size + if ((buf[(short) (offset + i)] & 0x80) != 0) { + // size encoded in many bytes + sizeforsize = (short) (buf[(short) (offset + i)] & 0x7F); + if ((short) (i+1+sizeforsize) >len) { + return 0; + } + if (sizeforsize > (short) 2) { + // more than two bytes for encoding => not something than we can handle + return 0; + } else if (sizeforsize == (short) 1) { + if ((short) (offset + i + 1 + sizeforsize) > len) { + return 0; + } + size = Util.makeShort((byte) 0,buf[(short) (offset + i + 1)]); + } else if (sizeforsize == 2) { + totalsize = (short) (i + 1 + sizeforsize + size); + if ((short) (offset + i + 1 + sizeforsize) > len) { + return (short) 0; + } + size = Util.getShort(buf, (short) (offset + i + 1)); + } + } else { + // size encode in one byte + size = Util.makeShort((byte) 0,buf[(short) (offset + i)]); + } + totalsize = (short) (i + 1 + sizeforsize + size); + if (totalsize < (short) 240 && (short) (offset + totalsize) > len) { + return (short) 0; + } + return totalsize; + } + + + public static boolean IsBERTLVTagEqual(byte[] tag, short offset, short length, byte[] value) { + short i = 1; + // first byte contains the class description + if (tag[offset] != value[(short)0]) { + return false; + } + // simplified tag + if ((value[(short) 0] & 0x1F ) != 0x1F) { + return true; + } + // subsequent bytes starts by 1xxxxxxx until the last byte 0xxxxxxx + while(((short) (offset + i) < length) && (tag[(short) (offset+i)] == value[i]) && ((value[i] & 0x80) != 0)) { + i++; + } + // check if the loop stopped because of 0xxxxxxx or an error + if (((short) (offset + i + 1) <= length) && (tag[(short) (offset+i)] == value[i])) { + return true; + } + return false; + } + + /* input data is supposed to have been validaded before */ + public static short GetBERTLVDataLen(byte[] buf, short offset, short len) { + short size = 0; + short sizeforsize = 0; + short i = 1; + + if ((buf[offset] & 0x1F) != 0x1F) { + // simplified tag + } else { + // tag start with all 5 last bits to 1 + // skip the tag + while (((buf[(short) (offset + i)] & 0x80) != 0) && ((short)(i+offset)) < len) { + i++; + } + // pass the last byte of the tag + i+=1; + } + // check the size + if ((buf[(short) (offset + i)] & 0x80) != 0) { + // size encoded in many bytes + sizeforsize = (short) (buf[(short) (offset + i)] & 0x7F); + if (sizeforsize > 2) { + // more than two bytes for encoding => not something than we can handle + return 0; + } else if (sizeforsize == 1) { + size = Util.makeShort((byte) 0,buf[(short) (offset + i + 1)]); + } else if (sizeforsize == 2) { + size = Util.getShort(buf, (short) (offset + i + 1)); + } + } else { + // size encode in one byte + size = Util.makeShort((byte) 0,buf[(short) (offset + i)]); + } + return size; + } +} diff --git a/src/com/mysmartlogon/gidsAppletTests/CryptoTest.java b/src/com/mysmartlogon/gidsAppletTests/CryptoTest.java new file mode 100644 index 0000000..74628a2 --- /dev/null +++ b/src/com/mysmartlogon/gidsAppletTests/CryptoTest.java @@ -0,0 +1,54 @@ +package com.mysmartlogon.gidsAppletTests; + +import org.junit.Before; +import org.junit.Test; + +public class CryptoTest extends GidsBaseTestClass { + + @Before + public void setUp() throws Exception { + super.setUp(); + createcard(); + } + + @Test + public void generateKeyAndReadItThenDeleteIt() { + // authenticate + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + execute("00 A4 00 0C 02 3F FF"); + // generate new key + execute("00 E0 00 00 49 62 47 82 01 18 83 02 B0 81 8C 05 8F 10 10 10 00 A5 37 B8 09 80 01 06 83 01 81 95 01 40 B8 09 80 01 86 83 01 81 95 01 40 B8 09 80 01 46 83 01 81 95 01 40 B6 09 80 01 16 83 01 81 95 01 40 B6 09 80 01 56 83 01 81 95 01 40"); + //execute("00 A4 00 0C 02 B0 81"); + execute("00 44 00 00 00"); + // generate asymetric key + execute("00 47 00 00 08 AC 06 80 01 06 83 01 81"); + execute("00 A4 00 0C 02 B0 81"); + // read the public key + execute("00 CB 3F FF 0A 70 08 84 01 81 A5 03 7F 49 80 00"); + // keymap file + execute("00 DB A0 00 10 DF 20 0D 01 01 00 00 00 06 9A 81 B0 FF FF 00 00"); + // cmapfile + execute("00 DB A0 10 59 DF 23 56 37 00 64 00 39 00 64 00 34 00 35 00 66 00 38 00 2D 00 66 00 36 00 64 00 66 00 2D 00 34 00 30 00 35 00 38 00 2D 00 39 00 65 00 61 00 39 00 2D 00 36 00 37 00 35 00 33 00 31 00 65 00 65 00 38 00 32 00 36 00 61 00 35 00 00 00 00 00 00 00 00 00 03 00 00 00 00 04"); + // deauthenticate + execute("00 20 00 82"); + + // authenticate + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + execute("00 A4 00 0C 02 3F FF"); + // select key + execute("00 A4 00 0C 02 B0 81"); + // delete it + execute("00 E4 00 00"); + // delete cmapfile + execute("00 DB A0 00 03 DF 20 00"); + // update cardcf file + execute("00 DB A0 10 09 DF 22 06 00 00 02 00 06 00"); + // clean cmapfile + execute("00 DB A0 10 59 DF 23 56 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"); + // deauthenticate + execute("00 20 00 82"); +} + + + +} diff --git a/src/com/mysmartlogon/gidsAppletTests/FileSystemTest.java b/src/com/mysmartlogon/gidsAppletTests/FileSystemTest.java new file mode 100644 index 0000000..49f6326 --- /dev/null +++ b/src/com/mysmartlogon/gidsAppletTests/FileSystemTest.java @@ -0,0 +1,240 @@ +package com.mysmartlogon.gidsAppletTests; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.Before; +import org.junit.Test; + +public class FileSystemTest extends GidsBaseTestClass { + + @Before + public void setUp() throws Exception { + super.setUp(); + createcard(); + } + + @Test + public void testACL() { + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // **mutual or external authentication only** + // get data + // never + authenticatePin(); + createKeyFile(0xB082, "87 00 20 FF"); + testPutKey(0x82, 0x6982); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x82, 0x9000); + deauthenticate(); + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // **pin authentication only** + // get data + // never + authenticatePin(); + createKeyFile(0xB083, "87 00 10 FF"); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x83, 0x6982); + deauthenticate(); + authenticatePin(); + testPutKey(0x83, 0x9000); + deauthenticate(); + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // pin authentication only or general authenticate + // get data + // never + authenticatePin(); + createKeyFile(0xB084, "87 00 30 FF"); + testPutKey(0x84, 0x9000); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x84, 0x9000); + deauthenticate(); + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // never + // get data + // never + authenticatePin(); + createKeyFile(0xB085, "87 00 FF FF"); + testPutKey(0x85, 0x6982); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x85, 0x6982); + deauthenticate(); + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // allowed + // get data + // never + authenticatePin(); + createKeyFile(0xB086, "87 00 00 FF"); + testPutKey(0x86, 0x9000); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x86, 0x9000); + deauthenticate(); + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // **pin authentication only** + // get data + // never + authenticatePin(); + createKeyFile(0xB087, "87 00 90 FF"); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x87, 0x6982); + deauthenticate(); + authenticatePin(); + testPutKey(0x87, 0x9000); + deauthenticate(); + + //ACL: 10000111 + // manage security environment + // no restriction + // put data + // **mutual or external authentication only** + // get data + // never + authenticatePin(); + createKeyFile(0xB088, "87 00 A0 FF"); + testPutKey(0x88, 0x6982); + deauthenticate(); + authenticateGeneral(); + testPutKey(0x88, 0x9000); + deauthenticate(); + + // no ACL + authenticatePin(); + execute("00E000001662148201188302B089A50BA4098001028301809501C0"); + execute("0044000000"); + testPutKey(0x89, 0x9000); + deauthenticate(); + testPutKey(0x89, 0x9000); + authenticateGeneral(); + testPutKey(0x89, 0x9000); + deauthenticate(); + + // media: contactonly operation + authenticatePin(); + createKeyFile(0xB090, "87 00 91 FF"); + deauthenticate(); + testPutKey(0x90, 0x6982); + simulator.changeProtocol("T=CL"); + testPutKey(0x90, 0x6982); + authenticatePin(); + testPutKey(0x90, 0x6982); + deauthenticate(); + simulator.changeProtocol("T=0"); + authenticatePin(); + testPutKey(0x90, 0x9000); + deauthenticate(); + + // media: contactless only operation + authenticatePin(); + createKeyFile(0xB091, "87 00 92 FF"); + deauthenticate(); + testPutKey(0x91, 0x6982); + authenticatePin(); + testPutKey(0x91, 0x6982); + deauthenticate(); + simulator.changeProtocol("T=CL"); + testPutKey(0x91, 0x6982); + authenticatePin(); + testPutKey(0x91, 0x9000); + deauthenticate(); + simulator.changeProtocol("T=0"); + } + + @Test + public void testPutGetData() { + authenticatePin(); + // create an empty DO + // check empty + execute("00CBA010045C02DF2400", 0x6a88); + // create + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", "DF24009000"); + // modify it + execute("00DBA01005DF2402ABCD00"); + // check content + execute("00CBA010045C02DF2400", "DF2402ABCD9000"); + // prune + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", "DF24009000"); + // delete + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", 0x6a88); + // create + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", "DF24009000"); + // delete + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", 0x6a88); + deauthenticate(); + } + + public void createKeyFile(int fileId, String acl) { + byte[] bacl = DatatypeConverter.parseHexBinary(acl.replaceAll("\\s","")); + + execute("00 E0 00 00 " + String.format("%02X", bacl.length + 0x18) + " 62" + String.format("%02X", bacl.length + 0x16) + "82 01 18 83 02" + String.format("%04X", fileId) + "8C " + String.format("%02X", bacl.length) + DatatypeConverter.printHexBinary(bacl) + "A5 0B A4 09 80 01 02 83 01 80 95 01 C0"); + execute("0044000000"); + } + + @Test + public void PutGetLargeData() { + // save a something large like a certificate using chained apdu + authenticatePin(); + execute("10 DB A0 10 F0 DF 24 82 02 9F 01 00 AA 02 78 DA 33 68 62 5A 66 D0 C4 D8 B7 80 99 89 91 89 49 C0 52 33 40 E6 B5 5B CE 0A A7 9E 87 FC DC C5 45 BD 06 BC 6C 9C 5A 6D 1E 6D DF 79 19 19 59 59 19 0C F8 0D 79 0D B8 D9 98 43 59 98 85 59 42 52 8B 4B 0C E4 C4 79 0D CD 0C 0C 0D 0D 80 A4 91 89 61 94 38 AF 11 32 17 53 43 13 A3 12 B2 A1 8C AC 0C CC 4D 8C FC 0C 40 71 2E A6 26 46 46 86 75 0B EB DE 18 D6 06 B7 F1 9D D5 FD 2E F5 20 29 FA 2C DB CF C5 8B 8C 3D 8A 0E 8B B3 7F 56 D3 FB 66 3C F1 A8 C4 84 37 7D 6B CB 73 8F 56 97 78 E7 C8 5F 73 3E E2 97 7A 55 52 7B 56 66 F2 AC FF 53 9F 33 5F 7B F1 A3 59 6E 41 F6 5E CD 42 86 15 FF 66 E8 F4 E6 DF 76 50 5F 75 38 EA EB 65 E1 35 5F 59 4A 27 F4 7D 8C 4F 59 17 FC C6 59 E8 4C 60 89 BD C3 97 CF DC F7 37 68 9C"); + execute("10 DB A0 10 F0 9A 2B 6B B2 65 21 CF BE 17 7B 75 94 1D 6C 26 54 2B 6E 8F BD 74 F7 83 56 B1 49 92 EE B9 3C 87 7F 96 86 67 2F DD 9E 7E 37 59 31 37 71 F7 AC 85 75 DF CF 4C E0 3B 71 F2 41 E5 F9 53 6E F2 E9 1E 1A 69 C5 0D 75 93 AA E7 D6 35 58 35 5C 13 ED AD 49 7A 9B 25 BD 67 BF F4 DD 8B 55 35 6A 91 BF 8F F5 DF E6 EF 0B 5B 92 1F 11 77 C3 ED 9B 7E D7 BF BD 66 06 EF 75 6E 36 9A AF E7 FD 72 EA 42 C9 8B D9 0F 6E 2E 70 B6 BC 5F 7D 9E 69 DF CD 49 5D 82 11 AB 8B 7F 2D 8E 64 62 66 64 60 44 0B 76 66 50 C0 4C 65 57 F8 1D 10 7E 38 FA 97 CF F2 E6 CA 29 46 BE AA 5E D9 4C 53 3C BC E7 48 D4 CD 0D 63 B0 FD CD F8 FA D7 F3 9E 7B BC 59 4F 9A D2 94 B7 7C 8F FE 54 91 11 38 F9 BB 77 C0 A2 1F FF F6 BC 58 91 F6 E1 93 CD D4 FF 01 B5 0F 36 7B 3E AB BF 55 74"); + execute("00 DB A0 10 C4 B7 72 AA A8 78 C9 7A 73 9B 1D 67 73 4D B7 B2 DE B6 0F 08 77 9E 70 7F E1 DE 95 1B BF 73 5D 68 E5 DD D1 5F C0 60 FA D9 FB DF 16 AE A7 E7 C2 F9 3A 5E ED 92 7F EE B4 F1 F4 9B 99 B2 13 CE 25 0A 4F 3D DD D9 9D DE 97 E3 DE 33 C1 E9 89 84 F8 8E 3F 06 E5 89 3C 93 1D CB 4F 74 A8 73 8A 31 04 AC EA B3 48 75 BD BA 60 E3 E3 05 2D FF 97 F7 FD 67 EB FC 5E BD 30 FC D0 65 83 A5 C2 0C 6E 6F FB 5F 3B DD 33 E0 F8 37 47 22 2C F9 D9 F2 57 7D B7 2C 56 C4 4D E8 7D FB 44 F4 97 3A 5B E3 EC F8 DF 2C 3F D7 F6 F1 BF E9 5E 75 50 E3 2A 9F 9D 4B C3 FE 6C 56 87 52 91 77 47 72 B9 96 DE FD EF BC 34 1C 00 4D 16 29 3D"); + deauthenticate(); + // read it and compare it + execute("00 CB A0 10 04 5C 02 DF 24 00", + "DF 24 82 02 9F 01 00 AA 02 78 DA 33 68 62 5A 66 D0 C4 D8 B7 80 99 89 91 89 49 C0 52 33 40 E6 B5 5B CE 0A A7 9E 87 FC DC C5 45 BD 06 BC 6C 9C 5A 6D 1E 6D DF 79 19 19 59 59 19 0C F8 0D 79 0D B8 D9 98 43 59 98 85 59 42 52 8B 4B 0C E4 C4 79 0D CD 0C 0C 0D 0D 80 A4 91 89 61 94 38 AF 11 32 17 53 43 13 A3 12 B2 A1 8C AC 0C CC 4D 8C FC 0C 40 71 2E A6 26 46 46 86 75 0B EB DE 18 D6 06 B7 F1 9D D5 FD 2E F5 20 29 FA 2C DB CF C5 8B 8C 3D 8A 0E 8B B3 7F 56 D3 FB 66 3C F1 A8 C4 84 37 7D 6B CB 73 8F 56 97 78 E7 C8 5F 73 3E E2 97 7A 55 52 7B 56 66 F2 AC FF 53 9F 33 5F 7B F1 A3 59 6E 41 F6 5E CD 42 86 15 FF 66 E8 F4 E6 DF 76 50 5F 75 38 EA EB 65 E1 35 5F 59 4A 27 F4 7D 8C 4F 59 17 FC C6 59 E8 4C 60 89 BD C3 97 CF DC F7 37 68 9C 9A 2B 6B B2 65 21 CF BE 17 7B 75 94 1D 6C 26 54 61 00"); + execute("00 C0 00 00 00", + "2B 6E 8F BD 74 F7 83 56 B1 49 92 EE B9 3C 87 7F 96 86 67 2F DD 9E 7E 37 59 31 37 71 F7 AC 85 75 DF CF 4C E0 3B 71 F2 41 E5 F9 53 6E F2 E9 1E 1A 69 C5 0D 75 93 AA E7 D6 35 58 35 5C 13 ED AD 49 7A 9B 25 BD 67 BF F4 DD 8B 55 35 6A 91 BF 8F F5 DF E6 EF 0B 5B 92 1F 11 77 C3 ED 9B 7E D7 BF BD 66 06 EF 75 6E 36 9A AF E7 FD 72 EA 42 C9 8B D9 0F 6E 2E 70 B6 BC 5F 7D 9E 69 DF CD 49 5D 82 11 AB 8B 7F 2D 8E 64 62 66 64 60 44 0B 76 66 50 C0 4C 65 57 F8 1D 10 7E 38 FA 97 CF F2 E6 CA 29 46 BE AA 5E D9 4C 53 3C BC E7 48 D4 CD 0D 63 B0 FD CD F8 FA D7 F3 9E 7B BC 59 4F 9A D2 94 B7 7C 8F FE 54 91 11 38 F9 BB 77 C0 A2 1F FF F6 BC 58 91 F6 E1 93 CD D4 FF 01 B5 0F 36 7B 3E AB BF 55 74 B7 72 AA A8 78 C9 7A 73 9B 1D 67 73 4D B7 B2 DE B6 0F 08 77 9E 70 7F E1 DE 95 1B BF 73 5D 68 E5 61 A4"); + execute("00 C0 00 00 A4", + "DD D1 5F C0 60 FA D9 FB DF 16 AE A7 E7 C2 F9 3A 5E ED 92 7F EE B4 F1 F4 9B 99 B2 13 CE 25 0A 4F 3D DD D9 9D DE 97 E3 DE 33 C1 E9 89 84 F8 8E 3F 06 E5 89 3C 93 1D CB 4F 74 A8 73 8A 31 04 AC EA B3 48 75 BD BA 60 E3 E3 05 2D FF 97 F7 FD 67 EB FC 5E BD 30 FC D0 65 83 A5 C2 0C 6E 6F FB 5F 3B DD 33 E0 F8 37 47 22 2C F9 D9 F2 57 7D B7 2C 56 C4 4D E8 7D FB 44 F4 97 3A 5B E3 EC F8 DF 2C 3F D7 F6 F1 BF E9 5E 75 50 E3 2A 9F 9D 4B C3 FE 6C 56 87 52 91 77 47 72 B9 96 DE FD EF BC 34 1C 00 4D 16 29 3D 90 00"); + authenticatePin(); + // prune + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", "DF24009000"); + // delete + execute("00DBA01003DF240000"); + // check content + execute("00CBA010045C02DF2400", 0x6a88); + deauthenticate(); + } + + private void testPutKey(int fileId, int expectedReturn) { + execute("00DB3FFF2670248401" + String.format("%02X", fileId) + "A51F87180102030405060708010203040506070801020304050607088803B073DC", expectedReturn); + } + +} diff --git a/src/com/mysmartlogon/gidsAppletTests/GidsBaseTestClass.java b/src/com/mysmartlogon/gidsAppletTests/GidsBaseTestClass.java new file mode 100644 index 0000000..0bf6018 --- /dev/null +++ b/src/com/mysmartlogon/gidsAppletTests/GidsBaseTestClass.java @@ -0,0 +1,187 @@ +package com.mysmartlogon.gidsAppletTests; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; +import javax.xml.bind.DatatypeConverter; + +import org.junit.Before; + +import com.licel.jcardsim.io.JavaxSmartCardInterface; +import com.mysmartlogon.gidsApplet.GidsApplet; + +import javacard.framework.AID; +import javacard.framework.Util; +import javacard.security.DESKey; +import javacard.security.KeyBuilder; +import javacard.security.RandomData; +import javacardx.crypto.Cipher; + +public abstract class GidsBaseTestClass { + + + protected JavaxSmartCardInterface simulator; + private boolean display = true; + + @Before + public void setUp() throws Exception { + // 1. Create simulator + byte[] TEST_APPLET_AID_BYTES = new byte[] {(byte) 0xA0,0x00,0x00,0x03,(byte) 0x97,0x42,0x54,0x46,0x59}; + AID TEST_APPLET_AID = new AID(TEST_APPLET_AID_BYTES, (short)0, (byte) TEST_APPLET_AID_BYTES.length); + + + + simulator = new JavaxSmartCardInterface (); + + // 2. Install applet + simulator.installApplet(TEST_APPLET_AID, GidsApplet.class); + simulator.selectApplet(TEST_APPLET_AID); + // 3. Select applet + } + + protected void createcard() { + //display = false; + execute("00A4040409A0000003974254465900"); + + execute("00240180083132333435363738"); + + execute("00E000000E620C82013983022F018C03032000"); + execute("0044000000"); + execute("00DB2F01034301F4"); + execute("00DB2F010547030801CC"); + execute("00DB2F010A46080102030405060708"); + + execute("00E000000E620C8201398302A0008C03033000"); + execute("0044000000"); + execute("00E000000E620C8201398302A0108C03033000"); + execute("0044000000"); + execute("00E000000E620C8201398302A0118C030330FF"); + execute("0044000000"); + execute("00E000000E620C8201398302A0128C03032000"); + execute("0044000000"); + execute("00E000000E620C8201398302A0138C03033030"); + execute("0044000000"); + execute("00E000000E620C8201398302A0148C03032020"); + execute("0044000000"); + // create admin key + execute("00 E0 00 00 1C 62 1A 82 01 18 83 02 B0 80 8C 04 87 00 20 FF A5 0B A4 09 80 01 02 83 01 80 95 01 C0"); + execute("0044000000"); + // set admin key + execute("00DB3FFF267024840180A51F87180102030405060708010203040506070801020304050607088803B073DC"); + // set masterfile + execute("00DBA00091DF1F818D016d736370000000000000000000000000000000000000000000a00000000000000000000000636172646964000000000020df000012a00000000000000000000000636172646170707300000021df000010a00000000000000000000000636172646366000000000022df000010a000006d7363700000000000636d617066696c6500000023df000010a00000"); + execute("00 DB A0 10 0B DF 21 08 6d 73 63 70 00 00 00 00"); + execute("00 DB A0 10 09 DF 22 06 00 00 00 00 00 00"); + execute("00 DB A0 10 03 DF 23 00"); + execute("00 DB A0 10 13 DF 20 10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"); + + // activate + execute("00 A4 00 0C 02 3F FF", false); + execute("00 44 00 00 00", false); + display = true; + } + + protected void authenticateGeneral() { + byte[] key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + authenticateGeneral(key, true); + } + + protected void authenticateMutual() { + byte[] key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + authenticateMutual(key, true); + } + + protected void authenticatePin() { + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + } + protected void deauthenticate() { + execute("00 20 00 82 00"); + } + + protected void authenticateMutual(byte[] key, boolean successexpected) { + byte[] myChallenge= new byte [16], globalchallenge = new byte[40], challengeresponse = new byte[40]; + byte[] challenge; + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey deskey = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + deskey.setKey(key, (short) 0); + RandomData randomData = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + randomData.generateData(myChallenge, (short) 0, (short) myChallenge.length); + // select admin key + execute("00 22 81 A4 03 83 01 80"); + // get a challenge + ResponseAPDU response = execute("00 87 00 00 14 7C 12 81 10" + DatatypeConverter.printHexBinary(myChallenge) + "00"); + if (!Arrays.equals(Arrays.copyOfRange(response.getBytes(), 0, 4), new byte[] {0x7C,0x12,(byte) 0x81,0x10})) { + fail("not a challenge:" + DatatypeConverter.printHexBinary(response.getBytes())); + } + // compute the response + challenge = Arrays.copyOfRange(response.getBytes(), 4, 20); + //solve challenge + //R2 + System.arraycopy(challenge, 0, globalchallenge, 0, 16); + //R1 + System.arraycopy(myChallenge, 0, globalchallenge, 16, 16); + // keep Z1 random + + cipherDES.init(deskey, Cipher.MODE_ENCRYPT); + cipherDES.doFinal(globalchallenge, (short) 0, (short)40, challengeresponse, (short) 0); + // send the response + execute("00 87 00 00 2C 7C 2A 82 28" + DatatypeConverter.printHexBinary(challengeresponse), (successexpected?0x9000: 0x6982)); + } + + protected void authenticateGeneral(byte[] key, boolean successexpected) { + byte[] challenge, challengeresponse = new byte[8]; + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey deskey = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + deskey.setKey(key, (short) 0); + + // select admin key + execute("00 22 81 A4 03 83 01 80"); + // get a challenge + ResponseAPDU response = execute("00 87 00 00 04 7C 02 81 00 00"); + if (!Arrays.equals(Arrays.copyOfRange(response.getBytes(), 0, 4), new byte[] {0x7C,0x0A,(byte) 0x81,0x08})) { + fail("not a challenge:" + DatatypeConverter.printHexBinary(response.getBytes())); + } + // compute the response + challenge = Arrays.copyOfRange(response.getBytes(), 4, 12); + //solve challenge + cipherDES.init(deskey, Cipher.MODE_ENCRYPT); + cipherDES.doFinal(challenge, (short) 0, (short)8, challengeresponse, (short) 0); + // send the response + execute("00 87 00 00 0C 7C 0A 82 08" + DatatypeConverter.printHexBinary(challengeresponse), (successexpected?0x9000: 0x6982)); + } + + protected void execute(String command, String expectedresponse) { + byte[] expected = DatatypeConverter.parseHexBinary(expectedresponse.replaceAll("\\s","")); + ResponseAPDU response = execute(command, 0xFFFF & Util.makeShort(expected[expected.length-2],expected[expected.length-1])); + if (!Arrays.equals(response.getBytes(), expected)) { + fail("expected: " + expectedresponse.replaceAll("\\s","") + " but was: " + DatatypeConverter.printHexBinary(response.getBytes())); + } + + } + + protected ResponseAPDU execute(String Command) { + return execute(Command,0x9000); + } + + protected ResponseAPDU execute(String Command, int expectedReturn) { + ResponseAPDU response = execute(Command,display); + if(response.getSW() != expectedReturn) { + fail("expected: " + Integer.toHexString(expectedReturn) + " but was: " + Integer.toHexString(response.getSW())); + } + return response; + } + + + + private ResponseAPDU execute(String Command, boolean display) { + + Command = Command.replaceAll("\\s",""); + if (display) System.out.println(Command); + ResponseAPDU response = simulator.transmitCommand(new CommandAPDU(DatatypeConverter.parseHexBinary(Command))); + if (display) System.out.println(DatatypeConverter.printHexBinary(response.getBytes())); + return response; + } +} diff --git a/src/com/mysmartlogon/gidsAppletTests/MinidriverTest.java b/src/com/mysmartlogon/gidsAppletTests/MinidriverTest.java new file mode 100644 index 0000000..ae390db --- /dev/null +++ b/src/com/mysmartlogon/gidsAppletTests/MinidriverTest.java @@ -0,0 +1,89 @@ +package com.mysmartlogon.gidsAppletTests; + +import org.junit.Before; +import org.junit.Test; + +public class MinidriverTest extends GidsBaseTestClass { + + @Before + public void setUp() throws Exception { + super.setUp(); + createcard(); + } + + @Test + public void readEmptyCard() { + execute("00 A4 04 00 09 A0 00 00 03 08 00 00 10 00", 0x6A82); + execute("00 A4 00 0C 02 3F FF"); + execute("00 A4 04 00 09 A0 00 00 03 97 42 54 46 59 00", "61 12 4F 0B A0 00 00 03 97 42 54 46 59 02 01 73 03 40 01 C0 90 00"); + execute("00 CB 3F FF 04 5C 02 7F 73 00", 0x6A88); + execute("00 CB A0 00 04 5C 02 DF 1F 00", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00"); +// execute("00 CB A0 12 04 5C 02 DF 20 00", +// "DF 20 10 A7 F5 C7 42 89 55 7B C0 7E 9A 9D E3 DD BD 21 D7 90 00"); + execute("00 CB A0 00 04 5C 02 DF 1F 00", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00"); + execute("00 CB A0 10 04 5C 02 DF 22 00 ", + "DF 22 06 00 00 00 00 00 00 90 00 "); + execute("00 CB A0 00 04 5C 02 DF 1F 00", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00"); + execute("00 CB A0 10 04 5C 02 DF 23 00 ", + "DF 23 00 90 00"); + execute("00 CB A0 00 04 5C 02 DF 1F 00 ", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00 "); + execute("00 CB A0 10 04 5C 02 DF 22 00 ", + "DF 22 06 00 00 00 00 00 00 90 00"); + execute("00 CB A0 00 04 5C 02 DF 1F 00 ", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00"); + execute("00 CB A0 10 04 5C 02 DF 23 00 ", + "DF 23 00 90 00 "); + execute("00 CB A0 00 04 5C 02 DF 1F 00 ", + "DF 1F 81 8D 01 6D 73 63 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 69 64 00 00 00 00 00 20 DF 00 00 12 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 61 70 70 73 00 00 00 21 DF 00 00 10 A0 00 00 00 00 00 00 00 00 00 00 00 63 61 72 64 63 66 00 00 00 00 00 22 DF 00 00 10 A0 00 00 6D 73 63 70 00 00 00 00 00 63 6D 61 70 66 69 6C 65 00 00 00 23 DF 00 00 10 A0 00 00 90 00"); + execute("00 CB A0 10 04 5C 02 DF 23 00", "DF 23 00 90 00"); + } + + @Test + public void importOneRSA2048Key() { + authenticatePin(); + execute("00 DB A0 10 09 DF 22 06 00 00 00 00 01 00"); + execute("00 DB A0 10 59 DF 23 56 32 00 35 00 37 00 66 00 30 00 33 00 36 00 32 00 2D 00 32 00 65 00 65 00 37 00 2D 00 34 00 34 00 66 00 35 00 2D 00 39 00 38 00 38 00 35 00 2D 00 33 00 36 00 66 00 61 00 34 00 39 00 30 00 66 00 33 00 30 00 63 00 31 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00"); + execute("00 DB A0 10 09 DF 22 06 00 00 00 00 02 00"); + execute("00 DB A0 10 59 DF 23 56 32 00 35 00 37 00 66 00 30 00 33 00 36 00 32 00 2D 00 32 00 65 00 65 00 37 00 2D 00 34 00 34 00 66 00 35 00 2D 00 39 00 38 00 38 00 35 00 2D 00 33 00 36 00 66 00 61 00 34 00 39 00 30 00 66 00 33 00 30 00 63 00 31 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00"); + execute("00 DB A0 10 09 DF 22 06 00 00 01 00 02 00"); + // create keyfile + execute("00 E0 00 00 49 62 47 82 01 18 83 02 B0 81 8C 05 8F 10 10 10 00 A5 37 B8 09 80 01 07 83 01 81 95 01 40 B8 09 80 01 87 83 01 81 95 01 40 B8 09 80 01 47 83 01 81 95 01 40 B6 09 80 01 17 83 01 81 95 01 40 B6 09 80 01 57 83 01 81 95 01 40"); + execute("00 44 00 00"); + // put the key + execute("10 DB 3F FF F0 70 82 04 B7 84 01 81 A5 82 04 B0 83 01 02 84 01 00 87 82 04 A6 30 82 04 A2 02 01 00 02 82 01 01 00 AE A1 7E EC 31 7D 53 86 0E CD 2D F7 1A E0 62 5B CD 06 F9 A3 A2 33 48 72 C3 17 07 F3 26 2E F6 33 91 C5 18 90 EC 8E AD 77 6D C5 7B 74 4B 6C 1F D6 43 C4 4E 65 D5 19 2B 9A 69 63 9A FF 95 E7 03 D6 E8 F8 83 1E A0 6B BD 29 71 00 A8 FE 98 2C 8D 6F DB 40 27 AA C3 5A F5 D3 13 AC F5 04 75 90 8E F1 5F 64 AE 53 EC 43 12 CC 51 74 3F 40 F4 F3 0B DF B0 28 CA 9D 1D 34 B4 A1 0C BE E8 BD 2C 23 40 3C 90 7B 21 B7 5D D2 DD F0 2A 73 34 62 2D CE 6E 40 FE 39 31 CD D2 DB 97 DD 63 21 6D 61 BB 9A A1 7E F7 CC 90 0E C8 C9 E0 79 CF CA 46 1F 67 48 28 66 73 80 7E 92 7B 9D 7E 80 3A 80 D6 15 8D 7C 62 ED 6A 1B BC BF 1B DD D1 7A 7C 26 59 FB C6 8F DB"); + execute("10 DB 3F FF F0 0F 8E 56 A4 6F 58 5E D8 46 F6 2F 8A FE BD 36 30 EF 2C D9 81 37 AF 0D F4 CA D0 74 E8 9B E0 D9 A0 43 39 DF 7B CF 02 BE D9 92 8A 11 58 AB 73 FA A3 59 02 03 01 00 01 02 82 01 00 18 21 78 C8 6A 8E 46 DC 5F 6F 3A CA CA 8B F7 0B 78 69 5E 55 9E 34 37 EE A3 C6 AA F2 8E 74 2E D1 31 73 6A D1 9B DE 9B 59 C5 71 64 7D CD 6E 43 77 F8 25 48 30 0A 22 50 44 26 0B FD 7F 13 D7 B8 50 1A 21 3C FD 1E 6D C4 D5 D5 27 A2 32 8C 14 9B F6 B8 F5 44 85 F4 38 BD 7F 4D 58 B9 02 C4 BC 15 04 85 8A 04 0C D4 B8 F0 00 0B C1 64 7C 35 5C E0 67 28 90 7A 6E FB 7A 2C FB 94 27 4D 41 D6 B7 88 48 9B 61 0A 86 61 4F 35 94 D0 27 AB 34 A7 9D 2A 5D D8 25 27 E0 F0 6B 84 FB 7E 78 00 D6 3D 53 95 57 6A ED BC 86 A1 6F 41 28 88 63 98 22 F1 A5 C0 44 54 E4 33 23 7D 8D"); + execute("10 DB 3F FF F0 3B D9 53 5B 44 AE 60 F9 AF 4F 04 B2 BD 66 A5 CB 24 5A F0 70 6D F0 C5 40 23 17 0C 58 D3 C7 03 1B 4D 23 C5 F5 11 FA 50 18 7B 97 00 D6 06 D5 05 4D 11 85 89 02 AD 63 05 E5 6C 0E 24 D3 18 7F 2D C5 28 23 9F D6 F7 3F 49 15 BF 81 02 81 81 00 EC 89 4A 0F DA 9A 31 AE DD BD 74 9E 54 C7 BD 5C EE 6C D0 E3 E7 17 18 32 14 CC 10 3D 16 E6 DF 29 26 BA 5E 79 0E 18 84 FB BB D3 A5 3E AC 7B A1 36 F4 12 D7 A6 09 2A 9C EC 64 63 AC A1 38 D2 1D 01 78 E7 65 9C 26 2F 27 C6 C5 DF 8D 74 F3 70 8A 32 D3 CF 7B 82 D2 50 16 E7 16 40 45 67 E6 FF 8F 40 44 28 2D 12 D9 E6 C0 18 27 22 84 53 73 39 08 92 A3 CD 96 20 F4 BD 22 AE 99 6B 5D FB 36 DC 47 1F 02 81 81 00 BD 00 26 24 EA A3 AC 24 30 C4 5A 8D F1 D9 DB 8C 3E C0 9E 40 FA CB 21 64 A6 6E B6 4B 97 C2"); + execute("10 DB 3F FF F0 9C 08 03 95 26 27 6D 2F 05 C9 CB 40 CA 4D E8 AF FA B0 41 AC BF 21 58 A5 B9 97 06 61 3B 46 09 5B 1C F8 0A 7B BE 72 79 40 03 FD 4D 05 B6 C8 51 49 98 93 05 29 14 1F 70 48 30 AA 6A 8A 2F 71 B0 97 FF 52 BD CE F6 A8 8F 70 BC C4 00 D5 D4 4A 98 CF 1F BA F6 D3 8E 52 83 7B 37 18 2E 50 4B 79 0C 94 9E 87 02 81 80 2C A7 2E 48 17 E8 8C 00 8A F3 BD 14 36 C5 0E 46 80 EC 21 F3 24 29 03 F4 50 60 C5 A0 02 B6 CA E2 25 E1 80 FC 31 61 07 99 CE 37 82 36 76 B1 50 19 E3 B1 BA EC 29 46 D6 20 FA 42 A6 D0 38 BD D1 A3 F9 83 15 77 88 ED D6 00 7C 52 5B 8D FC 76 FE 5F E2 04 85 38 89 23 7D 6F 5A 8F FF 8A 41 DD 74 D9 99 04 4C 9E 06 6C BD 4E D1 F4 7E 2E 92 CD ED 84 61 4F EA 4B 86 68 3E 27 DF E6 98 F3 57 C3 F5 D9 02 81 80 7F 19 22 8D A0 DD 00 5E"); + execute("10 DB 3F FF F0 36 0D 55 78 2F 33 FA 58 8D BA AA B1 B7 F5 F2 36 E5 55 7E 71 C7 54 AF E9 13 BF B1 30 C2 53 E4 E8 97 2E B2 DD 41 A5 56 48 41 62 12 06 32 BE FF D7 78 0A DA F9 76 41 37 71 F6 5A D0 EF DA 6A 1A 44 B5 0F 2A A3 FB 46 2A BC 73 B3 F4 3C 50 1D BB DC 5E 59 1F 1E AD 4B 0A 00 72 74 43 9B 87 1A A1 56 8D D9 3E 32 0C 20 FD 8D 95 7F 1F 2E 8E 0C C6 38 14 AD 1A 94 04 C9 CC F1 02 81 80 7D E1 05 C6 30 D6 00 68 E4 45 B8 A2 C2 19 DF 30 DB 1D 5F 66 F0 FA EB 38 7C 90 99 AD 21 C1 50 05 EE 65 85 EB CC 88 5E DD CD FD 41 86 9C 91 B8 33 0D 5D 26 BB F0 FA 71 6D 1B 26 B0 85 2D 5B 26 15 C3 51 F4 7B 14 46 64 E7 B1 AD BC CF 3F BC 3D 6D 48 FE 0F 3F 4C CF C0 39 23 5A 50 B2 69 CB 78 21 62 28 51 F1 2C 41 CE AC E3 19 B1 2B 0A 66 BA 0B CA 9D 91 70 CC"); + execute("00 DB 3F FF 0B D9 B8 75 89 1C 44 9A BD F5 94 49"); + // update may files + execute("00 DB A0 00 10 DF 20 0D 01 01 00 00 00 07 9A 81 B0 FF FF 00 00"); + execute("00 DB A0 10 09 DF 22 06 00 00 01 00 03 00"); + execute("00 DB A0 10 59 DF 23 56 32 00 35 00 37 00 66 00 30 00 33 00 36 00 32 00 2D 00 32 00 65 00 65 00 37 00 2D 00 34 00 34 00 66 00 35 00 2D 00 39 00 38 00 38 00 35 00 2D 00 33 00 36 00 66 00 61 00 34 00 39 00 30 00 66 00 33 00 30 00 63 00 31 00 00 00 00 00 00 00 00 00 03 00 00 00 00 08"); + // read the public key + execute("00 CB 3F FF 0A 70 08 84 01 81 A5 03 7F 49 80 00", + "7F 49 82 01 09 81 82 01 00 AE A1 7E EC 31 7D 53 86 0E CD 2D F7 1A E0 62 5B CD 06 F9 A3 A2 33 48 72 C3 17 07 F3 26 2E F6 33 91 C5 18 90 EC 8E AD 77 6D C5 7B 74 4B 6C 1F D6 43 C4 4E 65 D5 19 2B 9A 69 63 9A FF 95 E7 03 D6 E8 F8 83 1E A0 6B BD 29 71 00 A8 FE 98 2C 8D 6F DB 40 27 AA C3 5A F5 D3 13 AC F5 04 75 90 8E F1 5F 64 AE 53 EC 43 12 CC 51 74 3F 40 F4 F3 0B DF B0 28 CA 9D 1D 34 B4 A1 0C BE E8 BD 2C 23 40 3C 90 7B 21 B7 5D D2 DD F0 2A 73 34 62 2D CE 6E 40 FE 39 31 CD D2 DB 97 DD 63 21 6D 61 BB 9A A1 7E F7 CC 90 0E C8 C9 E0 79 CF CA 46 1F 67 48 28 66 73 80 7E 92 7B 9D 7E 80 3A 80 D6 15 8D 7C 62 ED 6A 1B BC BF 1B DD D1 7A 7C 26 59 FB C6 8F DB 0F 8E 56 A4 6F 58 5E D8 46 F6 2F 8A FE BD 36 30 EF 2C D9 81 37 AF 0D F4 CA D0 74 E8 9B E0 D9 A0 43 39 DF 7B CF 02 BE D9 61 0E"); + execute("00 C0 00 00 0E", + "92 8A 11 58 AB 73 FA A3 59 82 03 01 00 01 90 00"); + // sign some data + execute("00 22 41 B6 06 80 01 57 84 01 81"); + execute("00 2A 9E 9A 22 30 20 30 0C 06 08 2A 86 48 86 F7 0D 02 05 05 00 04 10 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC 00"); + execute("00 22 41 B6 06 80 01 57 84 01 81"); + execute("00 2A 9E 9A 33 30 31 30 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC 00"); + execute("00 22 41 B6 06 80 01 57 84 01 81"); + execute("00 2A 9E 9A 23 30 21 30 09 06 05 2B 0E 03 02 1A 05 00 04 14 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC 00"); + execute("00 22 41 B6 06 80 01 57 84 01 81"); + execute("00 2A 9E 9A 24 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC 00"); + // decrypt some data and verify the decryption + execute("00 22 41 B8 06 80 01 47 84 01 81"); + execute("10 2A 80 86 F0 3A 2F 91 1E 9A 76 FE F7 23 59 39 B5 BB A4 F6 F9 54 33 46 17 BA 34 7B EA 8E B5 6A AA B4 B0 D8 AA DE E1 0F 98 41 C2 85 36 B3 06 84 93 45 17 F5 63 59 5F 34 9B 94 4C D3 2A DF F7 35 FB F0 04 8A 95 13 E4 7C F9 D7 BA 61 E7 47 CB 42 AB 2B DA 99 19 15 EF 85 32 E6 EF 3C 29 D6 4F 46 A8 53 5C 71 BD 9B 15 A3 4C 01 3C D5 82 BA C8 0C 06 40 E6 09 69 A7 4C C9 4A 26 E6 73 0B A0 FD 17 0A 4F 70 AA 53 60 79 3D CB B7 D3 8B 93 3A B2 DD 63 98 49 DE D7 5B 56 BE 8C 2C 24 DE F1 64 E0 0F B4 69 60 12 A7 D3 FC 54 38 45 D7 25 68 B4 94 EA 67 1D 1C A3 FC 75 80 C5 3F B0 EF 17 DB 78 0B 7E 4F A2 7C FF 8D D5 A7 6D 03 58 F4 1D 85 4C 5C 61 7D 01 11 5E 7F 8D BF D9 A0 67 08 92 08 F5 C1 69 EF 42 8B BD 52 19 99 69 CE 06 8B F4 33 48 FC 1C D3 26 28 77 E6"); + execute("00 2A 80 86 10 44 DA 76 E1 C5 99 14 91 0A A6 93 8F 55 49 03 26 00", "74 65 73 74 31 32 33 34 35 36 37 38 39 30 00 00 00 00 00 00 90 00"); + + deauthenticate(); + } + +} diff --git a/src/com/mysmartlogon/gidsAppletTests/PinTests.java b/src/com/mysmartlogon/gidsAppletTests/PinTests.java new file mode 100644 index 0000000..8f8133f --- /dev/null +++ b/src/com/mysmartlogon/gidsAppletTests/PinTests.java @@ -0,0 +1,219 @@ +package com.mysmartlogon.gidsAppletTests; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import javax.smartcardio.ResponseAPDU; +import javax.xml.bind.DatatypeConverter; + +import org.junit.Before; +import org.junit.Test; + +import javacard.security.DESKey; +import javacard.security.KeyBuilder; +import javacard.security.RandomData; +import javacardx.crypto.Cipher; + + +public class PinTests extends GidsBaseTestClass { + + @Before + public void setUp() throws Exception { + super.setUp(); + createcard(); + } + + @Test + public void testVerifyPin() { + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + // pin status + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + } + + @Test + public void testTooLongPin() { + execute("00 20 00 80 20 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31", 0x63C3); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + execute("00 20 00 80 20 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 31", 0x63C3); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + } + + @Test + public void testTooShortPin() { + execute("00 20 00 80 01 30", 0x63C3); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + execute("00 20 00 80 01 30", 0x63C3); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + } + + @Test + public void testNonExistingPin() { + execute("00 20 00 79 08 31 32 33 34 35 36 37 38", 0x6a88); + // pin status + execute("00 CB 3F FF 04 5C 02 7F 69 00", 0x6984); + } + + @Test + public void testVerifyPuk() { + // puk is disabled + execute("00200081083132333435363738", 0x6a88); + execute("00 CB 3F FF 04 5C 02 7F 73 00", 0x6a88); + } + + @Test + public void AuthenticateAdminGeneral() { + authenticateGeneral(); + } + + @Test + public void AuthenticateAdminMutual() { + authenticateMutual(); + } + + @Test + public void WrongAuthenticateAdminGeneral() { + byte[] key = DatatypeConverter.parseHexBinary("000000000000000000000000000000000000000000000000"); + authenticateGeneral(key, false); + key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + authenticateGeneral(key, true); + } + + @Test + public void WrongAuthenticateAdminMutual() { + byte[] key = DatatypeConverter.parseHexBinary("000000000000000000000000000000000000000000000000"); + authenticateMutual(key, false); + key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + authenticateMutual(key, true); + } + + @Test + public void testPinFailure() { + // good PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x63C2); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701029301039000"); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x63C1); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701019301039000"); + // good PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + execute("00 CB 3F FF 04 5C 02 7F 71 00", "7F71069701039301039000"); + } + + @Test + public void testChangePIN() { + // good pin + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + // change pin + execute("00 24 00 80 10 31 32 33 34 35 36 37 38 31 32 33 34 35 36 37 37"); + // try old one + execute("00 20 00 80 08 31 32 33 34 35 36 37 38", 0x63C2); + // try new one + execute("00 20 00 80 08 31 32 33 34 35 36 37 37"); + // change pin again + execute("00 24 00 80 10 31 32 33 34 35 36 37 37 31 32 33 34 35 36 37 38"); + } + + @Test + public void testBlockPin() { + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x63C2); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x63C1); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x63C0); + // blocked + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x6983); + // bad PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 37", 0x6983); + + byte[] key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + authenticateGeneral(key, true); + + // unblock PIN + execute("00 2C 02 80 08 31 32 33 34 35 36 37 38"); + // test PIN + execute("00 20 00 80 08 31 32 33 34 35 36 37 38"); + } + + @Test + public void authenticateMutualReplayAttack() { + byte[] key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + byte[] myChallenge= new byte [16], globalchallenge = new byte[40], challengeresponse = new byte[40]; + byte[] challenge; + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey deskey = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + deskey.setKey(key, (short) 0); + RandomData randomData = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + randomData.generateData(myChallenge, (short) 0, (short) myChallenge.length); + // select admin key + execute("00 22 81 A4 03 83 01 80"); + // get a challenge + ResponseAPDU response = execute("00 87 00 00 14 7C 12 81 10" + DatatypeConverter.printHexBinary(myChallenge) + "00"); + if (!Arrays.equals(Arrays.copyOfRange(response.getBytes(), 0, 4), new byte[] {0x7C,0x12,(byte) 0x81,0x10})) { + fail("not a challenge:" + DatatypeConverter.printHexBinary(response.getBytes())); + } + // compute the response + challenge = Arrays.copyOfRange(response.getBytes(), 4, 20); + //solve challenge + //R2 + System.arraycopy(challenge, 0, globalchallenge, 0, 16); + //R1 + System.arraycopy(myChallenge, 0, globalchallenge, 16, 16); + // keep Z1 random + + cipherDES.init(deskey, Cipher.MODE_ENCRYPT); + cipherDES.doFinal(globalchallenge, (short) 0, (short)40, challengeresponse, (short) 0); + // send the response + execute("00 87 00 00 2C 7C 2A 82 28" + DatatypeConverter.printHexBinary(challengeresponse), 0x9000); + execute("00 87 00 00 2C 7C 2A 82 28" + DatatypeConverter.printHexBinary(challengeresponse), 0x6985); + } + + @Test + public void authenticateGeneralReplayAttack() { + byte[] challenge, challengeresponse = new byte[8]; + byte[] key = DatatypeConverter.parseHexBinary("010203040506070801020304050607080102030405060708"); + Cipher cipherDES = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD, false); + DESKey deskey = (DESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false); + deskey.setKey(key, (short) 0); + + // select admin key + execute("00 22 81 A4 03 83 01 80"); + // get a challenge + ResponseAPDU response = execute("00 87 00 00 04 7C 02 81 00 00"); + if (!Arrays.equals(Arrays.copyOfRange(response.getBytes(), 0, 4), new byte[] {0x7C,0x0A,(byte) 0x81,0x08})) { + fail("not a challenge:" + DatatypeConverter.printHexBinary(response.getBytes())); + } + // compute the response + challenge = Arrays.copyOfRange(response.getBytes(), 4, 12); + //solve challenge + cipherDES.init(deskey, Cipher.MODE_ENCRYPT); + cipherDES.doFinal(challenge, (short) 0, (short)8, challengeresponse, (short) 0); + // send the response + execute("00 87 00 00 0C 7C 0A 82 08" + DatatypeConverter.printHexBinary(challengeresponse), 0x9000); + execute("00 87 00 00 0C 7C 0A 82 08" + DatatypeConverter.printHexBinary(challengeresponse), 0x6985); + } + + @Test + public void bogusChallenge() { + execute("00 87 00 00 00", 0x6984); + execute("00 87 00 00 01 7c", 0x6984); + execute("00 87 00 00 02 7c 00", 0x6984); + execute("00 87 00 00 02 7c 01 00", 0x6984); + execute("00 87 00 00 04 7c 02 79 00", 0x6984); + execute("00 87 00 00 04 7c 02 82 00", 0x6984); + } + + @Test + public void bogusVerifyPin() { + execute("00 2C 00 81 01 31", 0x6a86); + } + +}