/* * GidsApplet: A Java Card implementation of the GIDS (Generic Identity * Device Specification) specification * https://msdn.microsoft.com/en-us/library/windows/hardware/dn642100%28v=vs.85%29.aspx * Copyright (C) 2016 Vincent Le Toux(vincent.letoux@mysmartlogon.com) * * It has been based on the IsoApplet * 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.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; /** * \brief class used to encapsulte authentication functions */ 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; // state for admin authentication private static final byte ADMIN_NOT_AUTHENTICATED = 0; 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: //no PUK on v2 of the card 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] = ADMIN_NOT_AUTHENTICATED; 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 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); } lc = apdu.setIncomingAndReceive(); 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, ISO7816.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; GidsPIN pin = null; lc = apdu.setIncomingAndReceive(); 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, ISO7816.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, ISO7816.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) (ISO7816.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; 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 lc = apdu.setIncomingAndReceive(); // 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, ISO7816.OFFSET_CDATA, (byte)lc); pin.resetAndUnblock(); // admin is deauthenticated at the end of the process DeauthenticateAllPin(); } else { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } } /** * \brief Process the general authentication process */ public void processGeneralAuthenticate(APDU apdu) { byte[] buf = apdu.getBuffer(); byte p1 = buf[ISO7816.OFFSET_P1]; byte p2 = buf[ISO7816.OFFSET_P2]; short lc; 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(); short innerPos = 0, innerLen = 0; if (buf[ISO7816.OFFSET_CDATA] != (byte) 0x7C) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } try { innerLen = UtilTLV.decodeLengthField(buf, (short) (ISO7816.OFFSET_CDATA+1)); innerPos = (short) (ISO7816.OFFSET_CDATA + 1 + UtilTLV.getLengthFieldLength(buf, (short) (ISO7816.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); } /** * \brief clear the data used for admin authentication */ 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); } /** * \brief handle the first part of the general authenticate APDU */ 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; } /** * \brief handle the second part of the general authenticate APDU */ 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])).GetSymmectricKey(), (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])).GetSymmectricKey(), (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; } /** * \brief return information regarding the PIN */ 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); } }