package pkgYkneoOath;
/*
* Copyright (c) 2013 Yubico AB
*
* 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, see <http://www.gnu.org/licenses/>.
*/
import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.framework.JCSystem;
import javacard.framework.Util;
import javacard.security.RandomData;
public class YkneoOath extends Applet {
public static final byte NAME_TAG = 0x71;
public static final byte NAME_LIST_TAG = 0x72;
public static final byte KEY_TAG = 0x73;
public static final byte CHALLENGE_TAG = 0x74;
public static final byte RESPONSE_TAG = 0x75;
public static final byte T_RESPONSE_TAG = 0x76;
public static final byte NO_RESPONSE_TAG = 0x77;
public static final byte PROPERTY_TAG = 0x78;
public static final byte VERSION_TAG = 0x79;
public static final byte IMF_TAG = 0x7a;
public static final byte PUT_INS = 0x01;
public static final byte DELETE_INS = 0x02;
public static final byte SET_CODE_INS = 0x03;
public static final byte RESET_INS = 0x04;
public static final byte LIST_INS = (byte)0xa1;
public static final byte CALCULATE_INS = (byte)0xa2;
public static final byte VALIDATE_INS = (byte)0xa3;
public static final byte CALCULATE_ALL_INS = (byte)0xa4;
public static final byte SEND_REMAINING_INS = (byte)0xa5;
private static final short _0 = 0;
private static final byte CHALLENGE_LENGTH = 8;
private byte[] tempBuf;
private byte[] sendBuffer;
private OathObj authObj;
private OathObj scratchAuth;
private byte[] propBuf;
private static final byte PROP_AUTH_OFFS = 0;
private static final byte PROP_SENT_DATA_OFFS = 1;
private static final byte PROP_REMAINING_DATA_LEN = 3;
private static final byte PROP_BUF_SIZE = PROP_REMAINING_DATA_LEN + 2;
private static final short BUFSIZE = 2048;
private static final short TMP_BUFSIZE = 32;
private RandomData rng;
private byte[] identity;
private static final byte[] version = {0x00,0x02,0x02};
public YkneoOath() {
tempBuf = JCSystem.makeTransientByteArray((short) TMP_BUFSIZE, JCSystem.CLEAR_ON_DESELECT);
sendBuffer = JCSystem.makeTransientByteArray(BUFSIZE, JCSystem.CLEAR_ON_DESELECT);
propBuf = JCSystem.makeTransientByteArray(PROP_BUF_SIZE, JCSystem.CLEAR_ON_DESELECT);
rng = RandomData.getInstance(RandomData.ALG_PSEUDO_RANDOM);
identity = new byte[CHALLENGE_LENGTH];
rng.generateData(identity, _0, CHALLENGE_LENGTH);
authObj = new OathObj();
scratchAuth = new OathObj();
}
public static void install(byte[] bArray, short bOffset, byte bLength) {
new YkneoOath().register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
public void process(APDU apdu) {
if (selectingApplet()) {
byte[] buf = apdu.getBuffer();
short offs = 0;
buf[offs++] = VERSION_TAG;
buf[offs++] = (byte)version.length;
Util.arrayCopyNonAtomic(version, _0, buf, offs, (short) version.length);
offs += (byte) version.length;
buf[offs++] = NAME_TAG;
short nameLen = (short) identity.length;
buf[offs++] = (byte) nameLen;
Util.arrayCopyNonAtomic(identity, _0, buf, offs, nameLen);
offs += nameLen;
// if the authobj is set add a challenge
if(authObj.isActive()) {
buf[offs++] = CHALLENGE_TAG;
buf[offs++] = CHALLENGE_LENGTH;
rng.generateData(buf, offs, CHALLENGE_LENGTH);
authObj.calculate(buf, offs, CHALLENGE_LENGTH, tempBuf, _0);
offs += CHALLENGE_LENGTH;
}
apdu.setOutgoingAndSend(_0, offs);
return;
}
byte[] buf = apdu.getBuffer();
apdu.setIncomingAndReceive();
short sendLen = 0;
byte p1 = buf[ISO7816.OFFSET_P1];
byte p2 = buf[ISO7816.OFFSET_P2];
short p1p2 = Util.makeShort(p1, p2);
byte ins = buf[ISO7816.OFFSET_INS];
if(authObj.isActive() && ins != VALIDATE_INS && ins != RESET_INS) {
if(propBuf[PROP_AUTH_OFFS] != 1) {
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}
}
switch (ins) {
case PUT_INS: // put
if(p1p2 == 0x0000) {
handlePut(buf);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case DELETE_INS: // delete
if(p1p2 == 0x0000) {
handleDelete(buf);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case SET_CODE_INS: // set code
if(p1p2 == 0x0000) {
handleChangeCode(buf);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case RESET_INS: // reset
if(p1p2 == (short)0xdead) {
handleReset();
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case LIST_INS: // list
if(p1p2 == 0x0000) {
sendLen = handleList(sendBuffer);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case CALCULATE_INS: // calculate
if(p1 == 0x00 && (p2 == 0x00 || p2 == 0x01)) {
sendLen = handleCalc(buf, p2, sendBuffer);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case VALIDATE_INS: // validate code
if(p1p2 == 0x0000) {
sendLen = handleValidate(buf, sendBuffer);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case CALCULATE_ALL_INS: // calculate all codes
if(p1 == 0x00 && (p2 == 0x00 || p2 == 0x01)) {
sendLen = handleCalcAll(buf, p2, sendBuffer);
} else {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
break;
case SEND_REMAINING_INS: // send data remaining in send buffer
sendLen = Util.getShort(propBuf, PROP_REMAINING_DATA_LEN);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
if(sendLen > 0) {
sendData(apdu, sendLen);
}
}
private void handleReset() {
authObj.setActive(false);
OathObj.firstObject = null;
OathObj.lastObject = null;
Util.arrayFillNonAtomic(propBuf, _0, PROP_BUF_SIZE, (byte)0);
rng.generateData(identity, _0, CHALLENGE_LENGTH);
JCSystem.requestObjectDeletion();
}
private short handleValidate(byte[] input, byte[] output) {
if(!authObj.isActive()) {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
short offs = 5;
if(input[offs++] != RESPONSE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short len = getLength(input, offs);
// make sure we're getting as long input as we expect
if(len != authObj.getDigestLength()) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
offs += getLengthBytes(len);
if(Util.arrayCompare(input, offs, tempBuf, _0, len) == 0) {
propBuf[PROP_AUTH_OFFS] = 1;
} else {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
offs += len;
if(input[offs++] != CHALLENGE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
len = getLength(input, offs);
// don't accept a challenge shorter than 8 bytes
if(len < 8) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
offs += getLengthBytes(len);
short respLen = authObj.calculate(input, offs, len, tempBuf, _0);
output[0] = RESPONSE_TAG;
output[1] = (byte) respLen;
Util.arrayCopyNonAtomic(tempBuf, _0, output, (short) 2, respLen);
return (short) (respLen + 2);
}
private void handleChangeCode(byte[] buf) {
short offs = 5;
if(buf[offs++] != KEY_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short len = getLength(buf, offs);
offs += getLengthBytes(len);
if(len == 0) {
authObj.setActive(false);
} else {
byte type = buf[offs++];
scratchAuth.setKey(buf, offs, type, (short) (len - 1));
offs += (short)(len - 1);
if(buf[offs++] != CHALLENGE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
len = getLength(buf, offs);
offs += getLengthBytes(len);
short respLen = scratchAuth.calculate(buf, offs, len, tempBuf, _0);
offs += len;
if(buf[offs++] != RESPONSE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
len = getLength(buf, offs);
offs += getLengthBytes(len);
if(len != respLen) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
if(Util.arrayCompare(buf, offs, tempBuf, _0, len) == 0) {
OathObj oldAuth = authObj;
authObj = scratchAuth;
scratchAuth = oldAuth;
oldAuth.setActive(false);
authObj.setActive(true);
} else {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
}
}
private short handleCalc(byte[] challenge, byte p2, byte[] output) {
short offs = 5;
if(challenge[offs++] != NAME_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short len = getLength(challenge, offs);
offs += getLengthBytes(len);
OathObj object = OathObj.findObject(challenge, offs, len);
if(object == null) {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
offs += len;
if(challenge[offs++] != CHALLENGE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
len = getLength(challenge, offs);
offs += getLengthBytes(len);
short respOffs = 0;
if(p2 == 0x00) {
len = object.calculate(challenge, offs, len, tempBuf, _0);
output[respOffs++] = RESPONSE_TAG;
} else {
len = object.calculateTruncated(challenge, offs, len, tempBuf, _0);
output[respOffs++] = T_RESPONSE_TAG;
}
respOffs += setLength(output, respOffs, (short) (len + 1));
output[respOffs++] = object.getDigits();
Util.arrayCopy(tempBuf, _0, output, respOffs, len);
return (short) (len + getLengthBytes(len) + 2);
}
private short handleCalcAll(byte[] challenge, byte p2, byte[] output) {
short offs = 5;
if(challenge[offs++] != CHALLENGE_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short chalLen = getLength(challenge, offs++);
Util.arrayCopyNonAtomic(challenge, offs, tempBuf, _0, chalLen);
offs = 0;
OathObj obj;
for(obj = OathObj.firstObject; obj != null; obj = obj.nextObject) {
if(!obj.isActive()) {
continue;
}
output[offs++] = NAME_TAG;
output[offs++] = (byte) obj.getNameLength();
offs += obj.getName(output, offs);
short len = 0;
if((obj.getType() & OathObj.OATH_MASK) == OathObj.TOTP_TYPE) {
if(p2 == 0x00) {
output[offs++] = RESPONSE_TAG;
len = obj.calculate(tempBuf, _0, chalLen, output, (short) (offs + 2));
} else {
output[offs++] = T_RESPONSE_TAG;
len = obj.calculateTruncated(tempBuf, _0, chalLen, output, (short) (offs + 2));
}
} else {
output[offs++] = NO_RESPONSE_TAG;
}
output[offs++] = (byte) (len + 1);
output[offs++] = obj.getDigits();
offs += len;
}
return offs;
}
private short handleList(byte[] output) {
short offs = 0;
OathObj object;
for(object = OathObj.firstObject; object != null; object = object.nextObject) {
if(!object.isActive()) {
continue;
}
output[offs++] = NAME_LIST_TAG;
output[offs++] = (byte) (object.getNameLength() + 1);
output[offs++] = object.getType();
offs += object.getName(output, offs);
}
return offs;
}
private void handleDelete(byte[] buf) {
short offs = ISO7816.OFFSET_CDATA;
if(buf[offs++] != NAME_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short len = getLength(buf, offs);
offs += getLengthBytes(len);
OathObj object = OathObj.findObject(buf, offs, len);
if(object != null) {
object.setActive(false);
} else {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
}
private short calculateTotalLen() {
short res = 0;
OathObj obj;
for(obj = OathObj.firstObject; obj != null; obj = obj.nextObject) {
if(!obj.isActive()) {
continue;
}
res += obj.getNameLength() + 9; // data and bytes add up to 9
}
return res;
}
private void handlePut(byte[] buf) {
short offs = ISO7816.OFFSET_CDATA;
if(buf[offs++] != NAME_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
short len = getLength(buf, offs);
offs += getLengthBytes(len);
if((short)(calculateTotalLen() + len + 9) > BUFSIZE) {
// the output will be longer than we can support, error out.
ISOException.throwIt(ISO7816.SW_FILE_FULL);
}
OathObj object = OathObj.findObject(buf, offs, len);
if(object == null) {
object = OathObj.getFreeObject();
object.setName(buf, offs, len);
}
offs += len;
if(buf[offs++] != KEY_TAG) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
len = getLength(buf, offs);
offs += getLengthBytes(len);
byte keyType = buf[offs++];
if((keyType & OathObj.HMAC_MASK) != OathObj.HMAC_SHA1 && (keyType & OathObj.HMAC_MASK) != OathObj.HMAC_SHA256) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
if((keyType & OathObj.OATH_MASK) != OathObj.TOTP_TYPE && (keyType & OathObj.OATH_MASK) != OathObj.HOTP_TYPE) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte digits = buf[offs++];
// protect against tearing (we want to do this as late as possible)
object.setActive(false);
object.setDigits(digits);
object.setKey(buf, offs, keyType, (short) (len - 2));
offs += (short)(len - 2);
if(offs < buf.length && buf[offs] == PROPERTY_TAG) {
offs++;
object.setProp(buf[offs++]);
} else {
object.setProp((byte) 0);
}
if(offs < buf.length && buf[offs] == IMF_TAG) {
offs++;
if(buf[offs++] == OathObj.IMF_LEN) {
object.setImf(buf, offs);
offs += OathObj.IMF_LEN;
} else {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
} else {
object.clearImf();
}
object.setActive(true);
}
private short getLength(byte[] buf, short offs) {
short length = 0;
if(buf[offs] <= 0x7f) {
length = buf[offs];
} else if(buf[offs] == (byte)0x81) {
length = buf[(short)(offs + 1)];
} else if(buf[offs] == (byte)0x82) {
length = Util.getShort(buf, (short) (offs + 1));
} else {
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
}
return length;
}
private short getLengthBytes(short len) {
if(len < (short)0x0080) {
return 1;
} else if(len <= (short)0x00ff) {
return 2;
} else {
return 3;
}
}
private short setLength(byte[] buf, short offs, short len) {
if(len < (short)0x0080) {
buf[offs] = (byte) len;
return 1;
} else if(len <= (short)0x00ff) {
buf[offs++] = (byte)0x81;
buf[offs] = (byte) len;
return 2;
} else {
buf[offs++] = (byte)0x82;
Util.setShort(buf, offs, len);
return 3;
}
}
private void sendData(APDU apdu, short len) {
byte[] buf = apdu.getBuffer();
short maxLen = APDU.getOutBlockSize();
short result;
short remainingData;
short toSend = maxLen;
short sentData = Util.getShort(propBuf, PROP_SENT_DATA_OFFS);
if(len < maxLen) {
toSend = len;
}
Util.arrayCopy(sendBuffer, sentData, buf, _0, toSend);
if(len > maxLen) {
remainingData = (short) (len - maxLen);
sentData += maxLen;
len = maxLen;
if(remainingData > maxLen) {
result = (short) (ISO7816.SW_BYTES_REMAINING_00 | maxLen);
} else {
result = (short) (ISO7816.SW_BYTES_REMAINING_00 | remainingData);
}
} else {
sentData = 0;
remainingData = 0;
result = ISO7816.SW_NO_ERROR;
}
Util.setShort(propBuf, PROP_SENT_DATA_OFFS, sentData);
Util.setShort(propBuf, PROP_REMAINING_DATA_LEN, remainingData);
apdu.setOutgoingAndSend(_0, len);
if(result != ISO7816.SW_NO_ERROR) {
ISOException.throwIt(result);
}
}
}