[WIP] rename hardwallet to keycard (#11)

* rename hardwallet to keycard

* rename hardwallet to keycard

* rename hardwallet to keycard

* change constants

* add convenience GlobalPlatform methods

* add javadoc to all classes
This commit is contained in:
Bitgamma
2018-12-07 16:44:02 +03:00
committed by GitHub
parent c749e5a744
commit 07fc087cb3
48 changed files with 1563 additions and 958 deletions
@@ -1,20 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import im.status.hardwallet_lite_android.wallet.TinyBERTLV;
public class ApplicationID {
public static final byte TLV_FILE_CONTROL_INFORMATION_TEMPLATE = (byte) 0x6F;
public static final byte TLV_APPLICATION_AID = (byte) 0x84;
private byte[] aid;
public ApplicationID(byte[] tlvData) throws IllegalArgumentException {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_FILE_CONTROL_INFORMATION_TEMPLATE);
this.aid = tlv.readPrimitive(TLV_APPLICATION_AID);
}
public byte[] getAID() {
return aid;
}
}
@@ -1,154 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import android.util.Base64;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import static android.util.Base64.NO_PADDING;
public class Crypto {
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
public static byte[] deriveSCP02SessionKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
byte[] key24 = resizeKey24(cardKey);
try {
byte[] derivationData = new byte[16];
// 2 bytes constant
System.arraycopy(purposeData, 0, derivationData, 0, 2);
// 2 bytes sequence counter + 12 bytes 0x00
System.arraycopy(seq, 0, derivationData, 2, 2);
SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8));
return cipher.doFinal(derivationData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException("error generating session keys.", e);
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
}
}
public static byte[] appendDESPadding(byte[] data) {
int paddingLength = 8 - (data.length % 8);
byte[] newData = new byte[data.length + paddingLength];
System.arraycopy(data, 0, newData, 0, data.length);
newData[data.length] = (byte)0x80;
return newData;
}
public static boolean verifyCryptogram(byte[] key, byte[] hostChallenge, byte[] cardChallenge, byte[] cardCryptogram) {
byte[] data = new byte[hostChallenge.length + cardChallenge.length];
System.arraycopy(hostChallenge, 0, data, 0, hostChallenge.length);
System.arraycopy(cardChallenge, 0, data, hostChallenge.length, cardChallenge.length);
byte[] paddedData = appendDESPadding(data);
byte[] calculated = mac3des(key, paddedData, NullBytes8);
return Arrays.equals(calculated , cardCryptogram);
}
public static byte[] mac3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data, 0, 24);
byte[] tail = new byte[8];
System.arraycopy(result, 16, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error calculating mac.", e);
}
}
public static byte[] macFull3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec keyDes = new SecretKeySpec(resizeKey8(keyData), "DES");
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding");
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding");
byte[] des3Iv = iv.clone();
if (data.length > 8) {
byte[] tmp = cipherDes.doFinal(data, 0, data.length - 8);
System.arraycopy(tmp, tmp.length - 8, des3Iv, 0, 8);
}
cipherDes3.init(Cipher.ENCRYPT_MODE, keyDes3, new IvParameterSpec(des3Iv));
byte[] result = cipherDes3.doFinal(data, data.length - 8, 8);
byte[] tail = new byte[8];
System.arraycopy(result, result.length - 8, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating full triple DES MAC.", e);
}
}
public static byte[] resizeKey24(byte[] keyData) {
byte[] key = new byte[24];
System.arraycopy(keyData, 0, key, 0, 16);
System.arraycopy(keyData, 0, key, 16, 8);
return key;
}
public static byte[] resizeKey8(byte[] keyData) {
byte[] key = new byte[8];
System.arraycopy(keyData, 0, key, 0, 8);
return key;
}
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
try {
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
SecretKeySpec key = new SecretKeySpec(resizeKey8(macKeyData), "DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(mac);
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating ICV.", e);
}
}
public static byte[] randomBytes(int length) {
SecureRandom random = new SecureRandom();
byte data[] = new byte[length];
random.nextBytes(data);
return data;
}
public static long randomLong(long bound) {
SecureRandom random = new SecureRandom();
return Math.abs(random.nextLong()) % bound;
}
public static String randomToken(int length) {
return Base64.encodeToString(randomBytes(length),NO_PADDING);
}
}
@@ -1,130 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import org.spongycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
public class GlobalPlatformCommandSet {
static final byte INS_SELECT = (byte) 0xA4;
static final byte INS_INITIALIZE_UPDATE = (byte) 0x50;
static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82;
static final byte INS_DELETE = (byte) 0xE4;
static final byte INS_INSTALL = (byte) 0xE6;
static final byte INS_LOAD = (byte) 0xE8;
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
static final byte INSTALL_FOR_LOAD_P1 = (byte) 0x02;
static final byte INSTALL_FOR_INSTALL_P1 = (byte) 0x0C;
static final byte LOAD_P1_MORE_BLOCKS = (byte) 0x00;
static final byte LOAD_P1_LAST_BLOCK = (byte) 0x80;
private final CardChannel apduChannel;
private SecureChannel secureChannel;
private SCP02Keys cardKeys;
private Session session;
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
public GlobalPlatformCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.cardKeys = new SCP02Keys(testKey, testKey);
}
public APDUResponse select() throws IOException {
APDUCommand cmd = new APDUCommand(0x00, INS_SELECT, SELECT_P1_BY_NAME, 0, new byte[0]);
return apduChannel.send(cmd);
}
public APDUResponse initializeUpdate(byte[] hostChallenge) throws IOException, APDUException {
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
APDUResponse resp = apduChannel.send(cmd);
if (resp.isOK()) {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
}
return resp;
}
public APDUResponse externalAuthenticate(byte[] hostChallenge) throws IOException {
byte[] cardChallenge = this.session.getCardChallenge();
byte[] data = new byte[cardChallenge.length + hostChallenge.length];
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
byte[] paddedData = Crypto.appendDESPadding(data);
byte[] hostCryptogram = Crypto.mac3des(this.session.getKeys().encKeyData, paddedData, Crypto.NullBytes8);
APDUCommand cmd = new APDUCommand(0x84, INS_EXTERNAL_AUTHENTICATE, EXTERNAL_AUTHENTICATE_P1, 0, hostCryptogram);
return this.secureChannel.send(cmd);
}
public APDUResponse delete(byte[] aid) throws IOException {
byte[] data = new byte[aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) aid.length;
System.arraycopy(aid, 0, data, 2, aid.length);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, 0, data);
return this.secureChannel.send(cmd);
}
public APDUResponse installForLoad(byte[] aid, byte[] sdaid) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(aid.length);
data.write(aid);
data.write(sdaid.length);
data.write(sdaid);
// empty hash length and hash
data.write(0x00);
data.write(0x00);
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_LOAD_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
public APDUResponse load(byte[] data, int count, boolean hasMoreBlocks) throws IOException {
int p1 = hasMoreBlocks ? LOAD_P1_MORE_BLOCKS : LOAD_P1_LAST_BLOCK;
APDUCommand cmd = new APDUCommand(0x80, INS_LOAD, p1, count, data);
return this.secureChannel.send(cmd);
}
public APDUResponse installForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(packageAID.length);
data.write(packageAID);
data.write(appletAID.length);
data.write(appletAID);
data.write(instanceAID.length);
data.write(instanceAID);
byte[] privileges = new byte[]{0x00};
data.write(privileges.length);
data.write(privileges);
byte[] fullParams = new byte[2 + params.length];
fullParams[0] = (byte) 0xC9;
fullParams[1] = (byte) params.length;
System.arraycopy(params, 0, fullParams, 2, params.length);
data.write(fullParams.length);
data.write(fullParams);
// empty perform token
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_INSTALL_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
}
@@ -1,138 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import im.status.hardwallet_lite_android.io.APDUCommand;
public class Load {
static final byte CLA = (byte) 0x80;
static final byte INS = (byte) 0xE8;
static final int BLOCK_SIZE = 147; // 255 - 8 bytes for MAC
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
private String path;
private int offset;
private int count;
private byte[] fullData;
public Load(InputStream in) throws FileNotFoundException, IOException {
this.path = path;
this.offset = 0;
this.count = 0;
Map<String, byte[]> files = this.loadFiles(in);
in.close();
this.fullData = this.getCode(files);
}
public Map<String, byte[]> loadFiles(InputStream in) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
ZipInputStream zip = new ZipInputStream(in);
ZipEntry entry = zip.getNextEntry();
while (entry != null) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count;
while ((count = zip.read(buf)) != -1) {
data.write(buf, 0, count);
}
String name = baseName(entry.getName());
files.put(name, data.toByteArray());
entry = zip.getNextEntry();
}
return files;
}
private String baseName(String path) {
String[] parts = path.split("[/.]");
return parts[parts.length - 2];
}
public int blocksCount() {
return (int) Math.ceil(this.fullData.length / (float) BLOCK_SIZE);
}
public byte[] nextDataBlock() {
if (this.offset >= this.fullData.length) {
return null;
}
int rangeEnd = this.offset + BLOCK_SIZE;
if (rangeEnd >= this.fullData.length) {
rangeEnd = this.fullData.length;
}
int size = rangeEnd - offset;
byte[] data = new byte[size];
System.arraycopy(this.fullData, this.offset, data, 0, size);
this.count++;
this.offset += size;
return data;
}
public boolean hasMore() {
return this.offset < this.fullData.length;
}
private byte[] encodeFullLength(int length) {
if (length < 0x80) {
return new byte[]{(byte) length};
} else if (length < 0xFF) {
return new byte[]{(byte) 0x81, (byte) length};
} else if (length < 0xFFFF) {
return new byte[]{
(byte) 0x82,
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
} else {
return new byte[]{
(byte) 0x83,
(byte) ((length & 0xFF0000) >> 16),
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
}
}
public byte[] getCode(Map<String, byte[]> files) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
for (String name : fileNames) {
byte[] fileData = files.get(name);
if (fileData == null) {
continue;
}
dataStream.write(fileData);
}
byte[] data = dataStream.toByteArray();
byte[] encodedFullLength = encodeFullLength(data.length);
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
fullData[0] = (byte) 0xC4;
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
return fullData;
}
public int getCount() {
return count;
}
}
@@ -1,19 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
public class SCP02Keys {
public byte[] encKeyData;
public byte[] macKeyData;
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
}
public byte[] getEncKeyData() {
return encKeyData;
}
public byte[] getMacKeyData() {
return macKeyData;
}
}
@@ -1,56 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import im.status.hardwallet_lite_android.io.APDUCommand;
public class SCP02Wrapper {
private byte[] macKeyData;
private byte[] icv;
public SCP02Wrapper(byte[] macKeyData) {
this.macKeyData = macKeyData;
this.icv = Crypto.NullBytes8.clone();
}
public APDUCommand wrap(APDUCommand cmd) {
try {
int cla = (cmd.getCla() | 0x04) & 0xff;
byte[] data = cmd.getData();
ByteArrayOutputStream macData = new ByteArrayOutputStream();
macData.write(cla);
macData.write(cmd.getIns());
macData.write(cmd.getP1());
macData.write(cmd.getP2());
macData.write(data.length + 8);
macData.write(data);
byte[] icv;
if (Arrays.equals(this.icv, Crypto.NullBytes8)) {
icv = this.icv;
} else {
icv = Crypto.encryptICV(this.macKeyData, this.icv);
}
byte[] mac = Crypto.macFull3des(this.macKeyData, Crypto.appendDESPadding(macData.toByteArray()), icv);
byte[] newData = new byte[data.length + mac.length];
System.arraycopy(data, 0, newData, 0, data.length );
System.arraycopy(mac, 0, newData, data.length, mac.length );
APDUCommand wrapped = new APDUCommand(cla, cmd.getIns(), cmd.getP1(), cmd.getP2(), newData, cmd.getNeedsLE());
this.icv = mac.clone();
return wrapped;
} catch (IOException e) {
throw new RuntimeException("error wrapping APDU command.", e);
}
}
public byte[] getICV() {
return this.icv;
}
}
@@ -1,64 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
import java.io.IOException;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
public class SecureChannel {
private CardChannel channel;
private SCP02Wrapper wrapper;
public static byte[] DERIVATION_PURPOSE_ENC = new byte[]{(byte) 0x01, (byte) 0x82};
public static byte[] DERIVATION_PURPOSE_MAC = new byte[]{(byte) 0x01, (byte) 0x01};
public static byte[] DERIVATION_PURPOSE_DEK = new byte[]{(byte) 0x01, (byte) 0x81};
public SecureChannel(CardChannel channel, SCP02Keys keys) {
this.channel = channel;
this.wrapper = new SCP02Wrapper(keys.getMacKeyData());
}
public APDUResponse send(APDUCommand cmd) throws IOException {
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
return this.channel.send(wrappedCommand);
}
public static Session verifyChallenge(byte[] hostChallenge, SCP02Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
throw new APDUException(resp.getSw(), "security condition not satisfied");
}
if (resp.getSw() == APDUResponse.SW_AUTHENTICATION_METHOD_BLOCKED) {
throw new APDUException(resp.getSw(), "authentication method blocked");
}
byte[] data = resp.getData();
if (data.length != 28) {
throw new APDUException(resp.getSw(), String.format("bad data length, expected 28, got %d", data.length));
}
byte[] cardChallenge = new byte[8];
System.arraycopy(data, 12, cardChallenge, 0, 8);
byte[] cardCryptogram = new byte[8];
System.arraycopy(data, 20, cardCryptogram, 0, 8);
byte[] seq = new byte[2];
System.arraycopy(data, 12, seq, 0, 2);
byte[] sessionEncKey = Crypto.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
throw new APDUException("error verifying card cryptogram.");
}
return new Session(sessionKeys, cardChallenge);
}
}
@@ -1,19 +0,0 @@
package im.status.hardwallet_lite_android.globalplatform;
public class Session {
private SCP02Keys keys;
private byte[] cardChallenge;
public Session(SCP02Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
}
public SCP02Keys getKeys() {
return keys;
}
public byte[] getCardChallenge() {
return cardChallenge;
}
}
@@ -1,67 +0,0 @@
package im.status.hardwallet_lite_android.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class APDUCommand {
protected int cla;
protected int ins;
protected int p1;
protected int p2;
protected int lc;
protected byte[] data;
protected boolean needsLE;
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
this(cla, ins, p1, p2, data, false);
}
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
this.cla = cla & 0xff;
this.ins = ins & 0xff;
this.p1 = p1 & 0xff;
this.p2 = p2 & 0xff;
this.data = data;
this.needsLE = needsLE;
}
public byte[] serialize() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(this.cla);
out.write(this.ins);
out.write(this.p1);
out.write(this.p2);
out.write(this.data.length);
out.write(this.data);
if (this.needsLE) {
out.write(0); // Response length
}
return out.toByteArray();
}
public int getCla() {
return cla;
}
public int getIns() {
return ins;
}
public int getP1() {
return p1;
}
public int getP2() {
return p2;
}
public byte[] getData() {
return data;
}
public boolean getNeedsLE() {
return this.needsLE;
}
}
@@ -1,15 +0,0 @@
package im.status.hardwallet_lite_android.io;
public class APDUException extends Exception {
public final int sw;
public APDUException(int sw, String message) {
super(message + ", 0x" + String.format("%04X", sw));
this.sw = sw;
}
public APDUException(String message) {
super(message);
this.sw = 0;
}
}
@@ -1,81 +0,0 @@
package im.status.hardwallet_lite_android.io;
public class APDUResponse {
public static final int SW_OK = 0x9000;
public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
public static final int SW_CARD_LOCKED = 0x6283;
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
private byte[] apdu;
private byte[] data;
private int sw;
private int sw1;
private int sw2;
public APDUResponse(byte[] apdu) {
if (apdu.length < 2) {
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
}
this.apdu = apdu;
this.parse();
}
private void parse() {
int length = this.apdu.length;
this.sw1 = this.apdu[length - 2] & 0xff;
this.sw2 = this.apdu[length - 1] & 0xff;
this.sw = (this.sw1 << 8) | this.sw2;
this.data = new byte[length - 2];
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
}
public boolean isOK() {
return this.sw == SW_OK;
}
public APDUResponse checkOK() throws APDUException {
this.checkSW(SW_OK);
return this;
}
public APDUResponse checkSW(int... codes) throws APDUException {
for (int code : codes) {
if (this.sw == code) {
return this;
}
}
switch (this.sw) {
case SW_SECURITY_CONDITION_NOT_SATISFIED:
throw new APDUException(this.sw, "security condition not satisfied");
case SW_AUTHENTICATION_METHOD_BLOCKED:
throw new APDUException(this.sw, "authentication method blocked");
default:
throw new APDUException(this.sw, "Unexpected error SW");
}
}
public byte[] getData() {
return this.data;
}
public int getSw() {
return this.sw;
}
public int getSw1() {
return this.sw1;
}
public int getSw2() {
return this.sw2;
}
public byte[] getBytes() {
return this.apdu;
}
}
@@ -1,30 +0,0 @@
package im.status.hardwallet_lite_android.io;
import android.nfc.tech.IsoDep;
import android.util.Log;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
public class CardChannel {
private static final String TAG = "CardChannel";
private IsoDep isoDep;
public CardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
byte[] resp = this.isoDep.transceive(apdu);
APDUResponse response = new APDUResponse(resp);
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
return response;
}
public boolean isConnected() {
return this.isoDep.isConnected();
}
}
@@ -1,6 +0,0 @@
package im.status.hardwallet_lite_android.io;
public interface CardListener {
void onConnected(CardChannel channel);
void onDisconnected();
}
@@ -1,86 +0,0 @@
package im.status.hardwallet_lite_android.io;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.security.Security;
import java.util.logging.Logger;
public class CardManager extends Thread implements NfcAdapter.ReaderCallback {
private static final String TAG = "CardManager";
private static final int DEFAULT_LOOP_SLEEP_MS = 50;
private IsoDep isoDep;
private boolean isRunning;
private CardListener cardListener;
private int loopSleepMS;
public boolean isConnected() {
return isoDep != null && isoDep.isConnected();
}
public CardManager() {
this(DEFAULT_LOOP_SLEEP_MS);
}
public CardManager(int loopSleepMS) {
this.loopSleepMS = loopSleepMS;
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
@Override
public void onTagDiscovered(Tag tag) {
isoDep = IsoDep.get(tag);
try {
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException e) {
Log.e(TAG, "error connecting to tag");
}
}
public void run() {
boolean connected = isConnected();
while (true) {
boolean newConnected = isConnected();
if (newConnected != connected) {
connected = newConnected;
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
if (connected && !isRunning) {
onCardConnected();
} else {
onCardDisconnected();
}
}
SystemClock.sleep(loopSleepMS);
}
}
private void onCardConnected() {
isRunning = true;
if (cardListener != null) {
cardListener.onConnected(new CardChannel(isoDep));
}
isRunning = false;
}
private void onCardDisconnected() {
isRunning = false;
isoDep = null;
if (cardListener != null) {
cardListener.onDisconnected();
}
}
public void setCardListener(CardListener listener) {
cardListener = listener;
}
}
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
/**
* Parses the response from a SELECT command. If the card has not yet received the INIT command the isInitializedCard
@@ -73,7 +73,7 @@ public class ApplicationInfo {
}
/**
* The public key to be used for secure channel opening. Usually handled internally by the WalletAppletCommandSet.
* The public key to be used for secure channel opening. Usually handled internally by the KeycardCommandSet.
*
* @return the public key
*/
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
/**
* Parses the result of a GET STATUS command retrieving application status.
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import org.spongycastle.crypto.digests.KeccakDigest;
import org.spongycastle.math.ec.ECPoint;
@@ -1,7 +1,7 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import java.io.IOException;
import java.security.SecureRandom;
@@ -24,8 +24,8 @@ public class CardDuplicator {
random.nextBytes(secret);
}
private WalletAppletCommandSet preamble(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(channel);
private KeycardCommandSet preamble(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.setPairing(pairing);
cmdSet.autoOpenSecureChannel();
@@ -45,7 +45,7 @@ public class CardDuplicator {
* @throws APDUException
*/
public void startDuplication(CardChannel channel, Pairing pairing, String pin, int deviceCount) throws IOException, APDUException {
WalletAppletCommandSet cmdSet = preamble(channel, pairing, pin);
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
cmdSet.duplicateKeyStart(deviceCount, secret).checkOK();
}
@@ -60,7 +60,7 @@ public class CardDuplicator {
* @throws APDUException
*/
public byte[] exportKey(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
WalletAppletCommandSet cmdSet = preamble(channel, pairing, pin);
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyExport().checkOK().getData();
}
@@ -75,7 +75,7 @@ public class CardDuplicator {
* @throws APDUException
*/
public byte[] importKey(CardChannel channel, Pairing pairing, String pin, byte[] key) throws IOException, APDUException {
WalletAppletCommandSet cmdSet = preamble(channel, pairing, pin);
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyImport(key).checkOK().getData();
}
@@ -88,7 +88,7 @@ public class CardDuplicator {
* @throws APDUException
*/
public void addEntropy(CardChannel channel) throws IOException, APDUException {
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(channel);
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
}
@@ -0,0 +1,22 @@
package im.status.keycard.applet;
import org.spongycastle.util.encoders.Hex;
public class Identifiers {
public static final byte[] PACKAGE_AID = Hex.decode("53746174757357616C6C6574");
public static final byte[] KEYCARD_AID = Hex.decode("53746174757357616C6C6574417070");
public static final byte[] NDEF_AID = Hex.decode("53746174757357616C6C65744E4643");
public static final byte[] NDEF_INSTANCE_AID = Hex.decode("D2760000850101");
/**
* Gets the instance AID of the Keycard applet. Since multiple instances this is a method instead of a constant.
* Soon a method taking an additional instance index will be added.
*
* @return the instance AID of the Keycard applet
*/
public static byte[] getKeycardInstanceAID() {
return KEYCARD_AID;
}
}
@@ -1,16 +1,16 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import java.util.StringTokenizer;
/**
* Keypath object to be used with the WalletAppletCommandSet
* Keypath object to be used with the KeycardCommandSet
*/
public class KeyPath {
private int source;
private byte[] data;
/**
* Parses a keypath into a byte array and source parameter to be used with the WalletAppletCommandSet object.
* Parses a keypath into a byte array and source parameter to be used with the KeycardCommandSet object.
*
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
*
@@ -32,16 +32,16 @@ public class KeyPath {
switch(sourceOrFirstElement) {
case "m":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_MASTER;
source = KeycardCommandSet.DERIVE_P1_SOURCE_MASTER;
break;
case "..":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT;
source = KeycardCommandSet.DERIVE_P1_SOURCE_PARENT;
break;
case ".":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT;
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
break;
default:
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT;
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
tokenizer = new StringTokenizer(keypath, "/"); // rewind
break;
}
@@ -65,7 +65,7 @@ public class KeyPath {
}
public KeyPath(byte[] data) {
this(data, WalletAppletCommandSet.DERIVE_P1_SOURCE_MASTER);
this(data, KeycardCommandSet.DERIVE_P1_SOURCE_MASTER);
}
private long parseComponent(String num) {
@@ -115,13 +115,13 @@ public class KeyPath {
StringBuffer sb = new StringBuffer();
switch(source) {
case WalletAppletCommandSet.DERIVE_P1_SOURCE_MASTER:
case KeycardCommandSet.DERIVE_P1_SOURCE_MASTER:
sb.append('m');
break;
case WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT:
case KeycardCommandSet.DERIVE_P1_SOURCE_PARENT:
sb.append("..");
break;
case WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT:
case KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT:
sb.append('.');
break;
}
@@ -1,9 +1,9 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.util.encoders.Hex;
@@ -13,7 +13,6 @@ import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
/**
@@ -21,7 +20,7 @@ import java.util.Arrays;
* file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
* pre/post processing.
*/
public class WalletAppletCommandSet {
public class KeycardCommandSet {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_SET_NDEF = (byte) 0xF3;
@@ -69,14 +68,10 @@ public class WalletAppletCommandSet {
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
public static final String APPLET_AID = "53746174757357616C6C6574417070";
static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
public WalletAppletCommandSet(CardChannel apduChannel) {
public KeycardCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.secureChannel = new SecureChannelSession();
}
@@ -108,7 +103,7 @@ public class WalletAppletCommandSet {
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES);
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID());
APDUResponse resp = apduChannel.send(selectApplet);
if (resp.getSw() == 0x9000) {
@@ -151,7 +146,7 @@ public class WalletAppletCommandSet {
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Status Hardware Wallet Lite".getBytes(), 50000, 32 * 8);
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), 50000, 32 * 8);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
@@ -1,6 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
import android.text.TextUtils;
package im.status.keycard.applet;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
@@ -99,7 +97,7 @@ public class Mnemonic {
* @return the mnemonic phrase
*/
public String toMnemonicPhrase() {
return TextUtils.join(" ", getWords());
return join(" ", getWords());
}
/**
@@ -152,4 +150,28 @@ public class Mnemonic {
return key.getEncoded();
}
/**
* String join. Used instead of Android TextUtils.join or Java 8 String.join method for compatibility reasons.
*
* @param list the list of words
* @param conjunction the conjunction
*
* @return the joined string
*/
private String join(String conjunction, String[] list) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String item : list) {
if (first) {
first = false;
} else {
sb.append(conjunction);
}
sb.append(item);
}
return sb.toString();
}
}
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import org.spongycastle.util.encoders.Base64;
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.asn1.x9.X9IntegerConverter;
@@ -1,9 +1,9 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.macs.CBCBlockCipherMac;
import org.spongycastle.crypto.params.KeyParameter;
@@ -1,4 +1,4 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import java.util.Arrays;
@@ -0,0 +1,219 @@
package im.status.keycard.globalplatform;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Crypto utilities for Global Platform.
*/
public class Crypto {
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
/**
* Derives a session key for SCP02.
*
* @param cardKey the key to derive
* @param seq the sequence number
* @param purposeData purpose data
*
* @return the derived key
*/
public static byte[] deriveSCP02SessionKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
byte[] key24 = resizeKey24(cardKey);
try {
byte[] derivationData = new byte[16];
// 2 bytes constant
System.arraycopy(purposeData, 0, derivationData, 0, 2);
// 2 bytes sequence counter + 12 bytes 0x00
System.arraycopy(seq, 0, derivationData, 2, 2);
SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8));
return cipher.doFinal(derivationData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException("error generating session keys.", e);
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
}
}
/**
* Padding for SCP02 encryption.
*
* @param data data to pad
* @return the padded data
*/
public static byte[] appendDESPadding(byte[] data) {
int paddingLength = 8 - (data.length % 8);
byte[] newData = new byte[data.length + paddingLength];
System.arraycopy(data, 0, newData, 0, data.length);
newData[data.length] = (byte)0x80;
return newData;
}
/**
* Verifies a card cryptogram received using during SCP02 channel establishment.
*
* @param key the key
* @param hostChallenge host challenge
* @param cardChallenge card challenge
* @param cardCryptogram cryptogram to verify
* @return true if correct, false otherwise
*/
public static boolean verifyCryptogram(byte[] key, byte[] hostChallenge, byte[] cardChallenge, byte[] cardCryptogram) {
byte[] data = new byte[hostChallenge.length + cardChallenge.length];
System.arraycopy(hostChallenge, 0, data, 0, hostChallenge.length);
System.arraycopy(cardChallenge, 0, data, hostChallenge.length, cardChallenge.length);
byte[] paddedData = appendDESPadding(data);
byte[] calculated = mac3des(key, paddedData, NullBytes8);
return Arrays.equals(calculated , cardCryptogram);
}
/**
* Calculates a 3DES MAC for SCP02 channel establishment
*
* @param keyData key
* @param data data to sign
* @param iv IV
* @return the MAC
*/
public static byte[] mac3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data, 0, 24);
byte[] tail = new byte[8];
System.arraycopy(result, 16, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error calculating mac.", e);
}
}
/**
* Generates a 3DES MAC for SCP02 communication
*
* @param keyData key
* @param data data to sign
* @param iv IV
* @return the MAC
*/
public static byte[] macFull3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec keyDes = new SecretKeySpec(resizeKey8(keyData), "DES");
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding");
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding");
byte[] des3Iv = iv.clone();
if (data.length > 8) {
byte[] tmp = cipherDes.doFinal(data, 0, data.length - 8);
System.arraycopy(tmp, tmp.length - 8, des3Iv, 0, 8);
}
cipherDes3.init(Cipher.ENCRYPT_MODE, keyDes3, new IvParameterSpec(des3Iv));
byte[] result = cipherDes3.doFinal(data, data.length - 8, 8);
byte[] tail = new byte[8];
System.arraycopy(result, result.length - 8, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating full triple DES MAC.", e);
}
}
/**
* Used during key derivation .
*
* @param keyData the key data
*
* @return the resized key
*/
private static byte[] resizeKey24(byte[] keyData) {
byte[] key = new byte[24];
System.arraycopy(keyData, 0, key, 0, 16);
System.arraycopy(keyData, 0, key, 16, 8);
return key;
}
/**
* Used during MAC generation.
*
* @param keyData the key data
*
* @return the resized key
*/
private static byte[] resizeKey8(byte[] keyData) {
byte[] key = new byte[8];
System.arraycopy(keyData, 0, key, 0, 8);
return key;
}
/**
* Encrypts the ICV
*
* @param macKeyData MAC Key
* @param mac mac
*
* @return encrypted ICV
*/
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
try {
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
SecretKeySpec key = new SecretKeySpec(resizeKey8(macKeyData), "DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(mac);
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating ICV.", e);
}
}
/**
* Generates the given number of random bytes.
*
* @param length the number of bytes to generate
* @return random bytes
*/
public static byte[] randomBytes(int length) {
SecureRandom random = new SecureRandom();
byte data[] = new byte[length];
random.nextBytes(data);
return data;
}
/**
* Generates a random long between 0 and then given boundary
*
* @param bound the maximum value to generate
* @return the random number
*/
public static long randomLong(long bound) {
SecureRandom random = new SecureRandom();
return Math.abs(random.nextLong()) % bound;
}
}
@@ -0,0 +1,316 @@
package im.status.keycard.globalplatform;
import im.status.keycard.applet.Identifiers;
import org.spongycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
/**
* Command set used for loading, installing and removing applets and packages. This class is generic and can work with
* any package and applet, but utility methods specific to the Keycard have been provided.
*/
public class GlobalPlatformCommandSet {
static final byte INS_SELECT = (byte) 0xA4;
static final byte INS_INITIALIZE_UPDATE = (byte) 0x50;
static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82;
static final byte INS_DELETE = (byte) 0xE4;
static final byte INS_INSTALL = (byte) 0xE6;
static final byte INS_LOAD = (byte) 0xE8;
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
static final byte INSTALL_FOR_LOAD_P1 = (byte) 0x02;
static final byte INSTALL_FOR_INSTALL_P1 = (byte) 0x0C;
static final byte LOAD_P1_MORE_BLOCKS = (byte) 0x00;
static final byte LOAD_P1_LAST_BLOCK = (byte) 0x80;
private final CardChannel apduChannel;
private SecureChannel secureChannel;
private SCP02Keys cardKeys;
private Session session;
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
/**
* Constructs a new command set with the given CardChannel.
*
* @param apduChannel the channel to the card
*/
public GlobalPlatformCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.cardKeys = new SCP02Keys(testKey, testKey);
}
/**
* Selects the ISD of the card.
*
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand cmd = new APDUCommand(0x00, INS_SELECT, SELECT_P1_BY_NAME, 0, new byte[0]);
return apduChannel.send(cmd);
}
/**
* Sends an INITIALIZE UPDATE command. Use the openSecureChannel method instead of calling this directly, unless you
* need to use a specific host challenge.
*
* @param hostChallenge the host challenge.
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse initializeUpdate(byte[] hostChallenge) throws IOException, APDUException {
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
APDUResponse resp = apduChannel.send(cmd);
if (resp.isOK()) {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
}
return resp;
}
/**
* Sends an EXTERNAL AUTHENTICATE command. Use the openSecureChannel method instead of calling this directly, unless you
* need to use a specific host challenge.
*
* @param hostChallenge the host challenge.
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse externalAuthenticate(byte[] hostChallenge) throws IOException {
byte[] cardChallenge = this.session.getCardChallenge();
byte[] data = new byte[cardChallenge.length + hostChallenge.length];
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
byte[] paddedData = Crypto.appendDESPadding(data);
byte[] hostCryptogram = Crypto.mac3des(this.session.getKeys().encKeyData, paddedData, Crypto.NullBytes8);
APDUCommand cmd = new APDUCommand(0x84, INS_EXTERNAL_AUTHENTICATE, EXTERNAL_AUTHENTICATE_P1, 0, hostCryptogram);
return this.secureChannel.send(cmd);
}
/**
* Opens an SCP02 secure channel with default keys.
*
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
* @throws IOException communication error
*/
public void openSecureChannel() throws APDUException, IOException {
SecureRandom random = new SecureRandom();
byte[] hostChallenge = new byte[8];
random.nextBytes(hostChallenge);
initializeUpdate(hostChallenge).checkOK();
externalAuthenticate(hostChallenge).checkOK();
}
/**
* Deletes the Keycard applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteKeycardInstance() throws IOException {
return delete(Identifiers.getKeycardInstanceAID());
}
/**
* Deletes the NDEF applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteNDEFInstance() throws IOException {
return delete(Identifiers.NDEF_INSTANCE_AID);
}
/**
* Deletes the Keycard package.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteKeycardPackage() throws IOException {
return delete(Identifiers.PACKAGE_AID);
}
/**
* Deletes the Keycard package and all applets installed from it. This is the method to use to remove a Keycard
* installation.
*
* @throws APDUException one of the DELETE commands failed
* @throws IOException communication error
*/
public void deleteKeycardInstancesAndPackage() throws IOException, APDUException {
deleteNDEFInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
deleteKeycardInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
}
/**
* Sends a DELETE APDU with the given AID
* @param aid the AID to the delete
* @return the raw card response
*
* @throws IOException communication error.
*/
public APDUResponse delete(byte[] aid) throws IOException {
byte[] data = new byte[aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) aid.length;
System.arraycopy(aid, 0, data, 2, aid.length);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, 0, data);
return this.secureChannel.send(cmd);
}
/**
* Loads the Keycard package.
*
* @param in the CAP file as an InputStream
* @param cb the progress callback
*
* @throws IOException communication error
* @throws APDUException one of the INSTALL [for Load] or LOAD commands failed
*/
public void loadKeycardPackage(InputStream in, LoadCallback cb) throws IOException, APDUException {
installForLoad(Identifiers.PACKAGE_AID).checkOK();
Load load = new Load(in);
byte[] block;
int steps = load.blocksCount();
while((block = load.nextDataBlock()) != null) {
load(block, (load.getCount() - 1), load.hasMore()).checkOK();
cb.blockLoaded(load.getCount(), steps);
}
}
/**
* Sends an INSTALL [for LOAD] APDU. Use only if loading something other than the Keycard package.
*
* @param aid the AID
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForLoad(byte[] aid) throws IOException {
return installForLoad(aid, new byte[0]);
}
/**
* Sends an INSTALL [for LOAD] APDU with package extradition. Use only if loading something other than the Keycard package.
*
* @param aid the AID
* @param sdaid the AID of the SD target of the extradition
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForLoad(byte[] aid, byte[] sdaid) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(aid.length);
data.write(aid);
data.write(sdaid.length);
data.write(sdaid);
// empty hash length and hash
data.write(0x00);
data.write(0x00);
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_LOAD_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* Sends a single LOAD APDU. Use only if loading something other than the Keycard package.
*
* @param data the data of the block
* @param count the block number
* @param hasMoreBlocks whether there are more blocks coming or not
* @return the card response
* @throws IOException communication error
*/
public APDUResponse load(byte[] data, int count, boolean hasMoreBlocks) throws IOException {
int p1 = hasMoreBlocks ? LOAD_P1_MORE_BLOCKS : LOAD_P1_LAST_BLOCK;
APDUCommand cmd = new APDUCommand(0x80, INS_LOAD, p1, count, data);
return this.secureChannel.send(cmd);
}
/**
* Sends an INSTALL [for Install & Make Selectable] command. Use only if not installing applets part of the Keycard
* package
*
* @param packageAID the package AID
* @param appletAID the applet AID
* @param instanceAID the instance AID
* @param params the installation parameters
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(packageAID.length);
data.write(packageAID);
data.write(appletAID.length);
data.write(appletAID);
data.write(instanceAID.length);
data.write(instanceAID);
byte[] privileges = new byte[]{0x00};
data.write(privileges.length);
data.write(privileges);
byte[] fullParams = new byte[2 + params.length];
fullParams[0] = (byte) 0xC9;
fullParams[1] = (byte) params.length;
System.arraycopy(params, 0, fullParams, 2, params.length);
data.write(fullParams.length);
data.write(fullParams);
// empty perform token
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_INSTALL_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* Installs the NDEF applet from the Keycard package.
*
* @param ndefRecord the initial NDEF record. Can be a zero-length array but not null
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installNDEFApplet(byte[] ndefRecord) throws IOException {
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.NDEF_AID, Identifiers.NDEF_INSTANCE_AID, ndefRecord);
}
/**
* Installs the Keycard applet.
*
* @return the card response
* @throws IOException communication error.
*/
public APDUResponse installKeycardApplet() throws IOException {
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.KEYCARD_AID, Identifiers.getKeycardInstanceAID(), new byte[0]);
}
}
@@ -0,0 +1,189 @@
package im.status.keycard.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* A loadable CAP file.
*/
public class Load {
static final byte CLA = (byte) 0x80;
static final byte INS = (byte) 0xE8;
static final int BLOCK_SIZE = 247; // 255 - 8 bytes for MAC
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
private int offset;
private int count;
private byte[] fullData;
/**
* Reads a CAP file from the given input stream.
*
* @param in the inpu stream
* @throws FileNotFoundException
* @throws IOException
*/
public Load(InputStream in) throws FileNotFoundException, IOException {
this.offset = 0;
this.count = 0;
Map<String, byte[]> files = this.loadFiles(in);
in.close();
this.fullData = this.getCode(files);
}
/**
* Reads the components of the CAP file
* @param in the input stream
* @return the map of component name and values
*
* @throws IOException IO error
*/
private Map<String, byte[]> loadFiles(InputStream in) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
ZipInputStream zip = new ZipInputStream(in);
ZipEntry entry = zip.getNextEntry();
while (entry != null) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count;
while ((count = zip.read(buf)) != -1) {
data.write(buf, 0, count);
}
String name = baseName(entry.getName());
files.put(name, data.toByteArray());
entry = zip.getNextEntry();
}
return files;
}
/**
* The basename of the zip entry
* @param path the path
* @return the base name
*/
private String baseName(String path) {
String[] parts = path.split("[/.]");
return parts[parts.length - 2];
}
/**
* Counts the number of blocks needed to load the entire file. Keeps in account the overhead of SCP02 secure channel
*
* @return the block count
*/
public int blocksCount() {
return (int) Math.ceil(this.fullData.length / (float) BLOCK_SIZE);
}
/**
* Returns the next data block
*
* @return the data block
*/
public byte[] nextDataBlock() {
if (this.offset >= this.fullData.length) {
return null;
}
int rangeEnd = this.offset + BLOCK_SIZE;
if (rangeEnd >= this.fullData.length) {
rangeEnd = this.fullData.length;
}
int size = rangeEnd - offset;
byte[] data = new byte[size];
System.arraycopy(this.fullData, this.offset, data, 0, size);
this.count++;
this.offset += size;
return data;
}
/**
* True if more blocks are present, false otherwise.
*
* @return true if more blocks are present, false otherwise.
*/
public boolean hasMore() {
return this.offset < this.fullData.length;
}
/**
* Encodes the length of the load TLV component
*
* @param length the length as integer
* @return the length encoded as for BER-TLV
*/
private byte[] encodeFullLength(int length) {
if (length < 0x80) {
return new byte[]{(byte) length};
} else if (length < 0xFF) {
return new byte[]{(byte) 0x81, (byte) length};
} else if (length < 0xFFFF) {
return new byte[]{
(byte) 0x82,
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
} else {
return new byte[]{
(byte) 0x83,
(byte) ((length & 0xFF0000) >> 16),
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
}
}
/**
* Serializes the CAP section in a single block.
*
* @param files the components to serialize
* @return the serialized load file
*
*/
private byte[] getCode(Map<String, byte[]> files) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
for (String name : fileNames) {
byte[] fileData = files.get(name);
if (fileData == null) {
continue;
}
dataStream.write(fileData);
}
byte[] data = dataStream.toByteArray();
byte[] encodedFullLength = encodeFullLength(data.length);
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
fullData[0] = (byte) 0xC4;
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
return fullData;
}
/**
* Returns the current block number
*
* @return the current block number
*/
public int getCount() {
return count;
}
}
@@ -0,0 +1,14 @@
package im.status.keycard.globalplatform;
/**
* Callback interface using during package loading process.
*/
public interface LoadCallback {
/**
* Called when a block is loaded.
*
* @param loadedBlock The number of the loaded block (1 based)
* @param blockCount the total number of blocks.
*/
void blockLoaded(int loadedBlock, int blockCount);
}
@@ -0,0 +1,37 @@
package im.status.keycard.globalplatform;
/**
* Keeps keys for SCP02.
*/
public class SCP02Keys {
public byte[] encKeyData;
public byte[] macKeyData;
/**
* Constructor. Takes the ENC and MAC keys.
*
* @param encKeyData encryption key
* @param macKeyData mac key
*/
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
}
/**
* The encryption key
* @return the encryption key
*/
public byte[] getEncKeyData() {
return encKeyData;
}
/**
* The MAC key
*
* @return the MAC key
*/
public byte[] getMacKeyData() {
return macKeyData;
}
}
@@ -0,0 +1,73 @@
package im.status.keycard.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import im.status.keycard.io.APDUCommand;
/**
* Adds a SCP02 MAC to APDUs.
*/
public class SCP02Wrapper {
private byte[] macKeyData;
private byte[] icv;
/**
* Constructs a new SCP02Wrapper.
*
* @param macKeyData the MAC key
*/
public SCP02Wrapper(byte[] macKeyData) {
this.macKeyData = macKeyData;
this.icv = Crypto.NullBytes8.clone();
}
/**
* Wraps an APDU with SCP02 MAC
* @param cmd the APDU to wrap
* @return the wrapped APDU
*/
public APDUCommand wrap(APDUCommand cmd) {
try {
int cla = (cmd.getCla() | 0x04) & 0xff;
byte[] data = cmd.getData();
ByteArrayOutputStream macData = new ByteArrayOutputStream();
macData.write(cla);
macData.write(cmd.getIns());
macData.write(cmd.getP1());
macData.write(cmd.getP2());
macData.write(data.length + 8);
macData.write(data);
byte[] icv;
if (Arrays.equals(this.icv, Crypto.NullBytes8)) {
icv = this.icv;
} else {
icv = Crypto.encryptICV(this.macKeyData, this.icv);
}
byte[] mac = Crypto.macFull3des(this.macKeyData, Crypto.appendDESPadding(macData.toByteArray()), icv);
byte[] newData = new byte[data.length + mac.length];
System.arraycopy(data, 0, newData, 0, data.length );
System.arraycopy(mac, 0, newData, data.length, mac.length );
APDUCommand wrapped = new APDUCommand(cla, cmd.getIns(), cmd.getP1(), cmd.getP2(), newData, cmd.getNeedsLE());
this.icv = mac.clone();
return wrapped;
} catch (IOException e) {
throw new RuntimeException("error wrapping APDU command.", e);
}
}
/**
* Returns the ICV
* @return the ICV
*/
public byte[] getICV() {
return this.icv;
}
}
@@ -0,0 +1,90 @@
package im.status.keycard.globalplatform;
import java.io.IOException;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
/**
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the scure channel.
*/
public class SecureChannel {
private CardChannel channel;
private SCP02Wrapper wrapper;
public static byte[] DERIVATION_PURPOSE_ENC = new byte[]{(byte) 0x01, (byte) 0x82};
public static byte[] DERIVATION_PURPOSE_MAC = new byte[]{(byte) 0x01, (byte) 0x01};
public static byte[] DERIVATION_PURPOSE_DEK = new byte[]{(byte) 0x01, (byte) 0x81};
/**
* Constructs an SCP02 secure channel, wrapping a regular CardChannel.
*
* @param channel the channel to wrap
* @param keys the keys
*/
public SecureChannel(CardChannel channel, SCP02Keys keys) {
this.channel = channel;
this.wrapper = new SCP02Wrapper(keys.getMacKeyData());
}
/**
* Protects the given command with SCP02 and forwards it to the underlying CardChannel.
*
* @param cmd the command to send
* @return the response from the card
*
* @throws IOException communication error
*/
public APDUResponse send(APDUCommand cmd) throws IOException {
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
return this.channel.send(wrappedCommand);
}
/**
* Verifies the card challenge and builds an SCP02 session object.
*
* @param hostChallenge the host challenge
* @param cardKeys the SCP02 keys
* @param resp the response from the card to the INITIALIZE UPDATE oommand
* @return
* @throws APDUException
*/
public static Session verifyChallenge(byte[] hostChallenge, SCP02Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
throw new APDUException(resp.getSw(), "security condition not satisfied");
}
if (resp.getSw() == APDUResponse.SW_AUTHENTICATION_METHOD_BLOCKED) {
throw new APDUException(resp.getSw(), "authentication method blocked");
}
byte[] data = resp.getData();
if (data.length != 28) {
throw new APDUException(resp.getSw(), String.format("bad data length, expected 28, got %d", data.length));
}
byte[] cardChallenge = new byte[8];
System.arraycopy(data, 12, cardChallenge, 0, 8);
byte[] cardCryptogram = new byte[8];
System.arraycopy(data, 20, cardCryptogram, 0, 8);
byte[] seq = new byte[2];
System.arraycopy(data, 12, seq, 0, 2);
byte[] sessionEncKey = Crypto.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
throw new APDUException("error verifying card cryptogram.");
}
return new Session(sessionKeys, cardChallenge);
}
}
@@ -0,0 +1,36 @@
package im.status.keycard.globalplatform;
/**
* SCP02 Session.
*/
public class Session {
private SCP02Keys keys;
private byte[] cardChallenge;
/**
* Constructs the SCP02 session.
*
* @param keys the session keys
* @param cardChallenge the card challenge
*/
public Session(SCP02Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
}
/**
* The SCP02 keys
* @return SCP02 keys
*/
public SCP02Keys getKeys() {
return keys;
}
/**
* The card challenge
* @return card challenge
*/
public byte[] getCardChallenge() {
return cardChallenge;
}
}
@@ -0,0 +1,125 @@
package im.status.keycard.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* ISO7816-4 APDU.
*/
public class APDUCommand {
protected int cla;
protected int ins;
protected int p1;
protected int p2;
protected int lc;
protected byte[] data;
protected boolean needsLE;
/**
* Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
*
* @param cla class byte
* @param ins instruction code
* @param p1 P1 parameter
* @param p2 P2 parameter
* @param data the APDU data
*/
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
this(cla, ins, p1, p2, data, false);
}
/**
* Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
* The LE byte, if sent, is set to 0.
*
* @param cla class byte
* @param ins instruction code
* @param p1 P1 parameter
* @param p2 P2 parameter
* @param data the APDU data
* @param needsLE whether the LE byte should be sent or not
*/
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
this.cla = cla & 0xff;
this.ins = ins & 0xff;
this.p1 = p1 & 0xff;
this.p2 = p2 & 0xff;
this.data = data;
this.needsLE = needsLE;
}
/**
* Serializes the APDU in order to send it to the card.
*
* @return the byte array representation of the APDU
*/
public byte[] serialize() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(this.cla);
out.write(this.ins);
out.write(this.p1);
out.write(this.p2);
out.write(this.data.length);
out.write(this.data);
if (this.needsLE) {
out.write(0); // Response length
}
return out.toByteArray();
}
/**
* Returns the CLA of the APDU
*
* @return the CLA of the APDU
*/
public int getCla() {
return cla;
}
/**
* Returns the INS of the APDU
*
* @return the INS of the APDU
*/
public int getIns() {
return ins;
}
/**
* Returns the P1 of the APDU
*
* @return the P1 of the APDU
*/
public int getP1() {
return p1;
}
/**
* Returns the P2 of the APDU
*
* @return the P2 of the APDU
*/
public int getP2() {
return p2;
}
/**
* Returns the data field of the APDU
*
* @return the data field of the APDU
*/
public byte[] getData() {
return data;
}
/**
* Returns whether LE is sent or not.
*
* @return whether LE is sent or not
*/
public boolean getNeedsLE() {
return this.needsLE;
}
}
@@ -0,0 +1,29 @@
package im.status.keycard.io;
/**
* Exception thrown when the response APDU from the card contains unexpected SW or data.
*/
public class APDUException extends Exception {
public final int sw;
/**
* Creates an exception with SW and message.
*
* @param sw the status word
* @param message a descriptive message of the error
*/
public APDUException(int sw, String message) {
super(message + ", 0x" + String.format("%04X", sw));
this.sw = sw;
}
/**
* Creates an exception with a message.
*
* @param message a descriptive message of the error
*/
public APDUException(String message) {
super(message);
this.sw = 0;
}
}
@@ -0,0 +1,132 @@
package im.status.keycard.io;
/**
* ISO7816-4 APDU response.
*/
public class APDUResponse {
public static final int SW_OK = 0x9000;
public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
public static final int SW_CARD_LOCKED = 0x6283;
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
private byte[] apdu;
private byte[] data;
private int sw;
private int sw1;
private int sw2;
/**
* Creates an APDU object by parsing the raw response from the card.
*
* @param apdu the raw response from the card.
*/
public APDUResponse(byte[] apdu) {
if (apdu.length < 2) {
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
}
this.apdu = apdu;
this.parse();
}
/**
* Parses the APDU response, separating the response data from SW.
*/
private void parse() {
int length = this.apdu.length;
this.sw1 = this.apdu[length - 2] & 0xff;
this.sw2 = this.apdu[length - 1] & 0xff;
this.sw = (this.sw1 << 8) | this.sw2;
this.data = new byte[length - 2];
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
}
/**
* Returns true if the SW is 0x9000.
*
* @return true if the SW is 0x9000.
*/
public boolean isOK() {
return this.sw == SW_OK;
}
/**
* Asserts that the SW is 0x9000. Throws an exception if it isn't
*
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkOK() throws APDUException {
return this.checkSW(SW_OK);
}
/**
* Asserts that the SW is contained in the given list. Throws an exception if it isn't.
*
* @param codes the list of SWs to match.
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkSW(int... codes) throws APDUException {
for (int code : codes) {
if (this.sw == code) {
return this;
}
}
switch (this.sw) {
case SW_SECURITY_CONDITION_NOT_SATISFIED:
throw new APDUException(this.sw, "security condition not satisfied");
case SW_AUTHENTICATION_METHOD_BLOCKED:
throw new APDUException(this.sw, "authentication method blocked");
default:
throw new APDUException(this.sw, "Unexpected error SW");
}
}
/**
* Returns the data field of this APDU.
*
* @return the data field of this APDU
*/
public byte[] getData() {
return this.data;
}
/**
* Returns the Status Word.
*
* @return the status word
*/
public int getSw() {
return this.sw;
}
/**
* Returns the SW1 byte
* @return SW1
*/
public int getSw1() {
return this.sw1;
}
/**
* Returns the SW2 byte
* @return SW2
*/
public int getSw2() {
return this.sw2;
}
/**
* Returns the raw unparsed response.
*
* @return raw APDU data
*/
public byte[] getBytes() {
return this.apdu;
}
}
@@ -0,0 +1,23 @@
package im.status.keycard.io;
import java.io.IOException;
/**
* A channel to transcieve ISO7816-4 APDUs.
*/
public interface CardChannel {
/**
* Sends the given C-APDU and returns an R-APDU.
*
* @param cmd the command to send
* @return the card response
* @throws IOException communication error
*/
APDUResponse send(APDUCommand cmd) throws IOException;
/**
* True if connected, false otherwise
* @return true if connected, false otherwise
*/
boolean isConnected();
}
@@ -0,0 +1,18 @@
package im.status.keycard.io;
/**
* Listener for card connection events.
*/
public interface CardListener {
/**
* Executes when the card channel is connected.
*
* @param channel the connected card channel
*/
void onConnected(CardChannel channel);
/**
* Executes when a previously connected card is disconnected.
*/
void onDisconnected();
}
@@ -0,0 +1,34 @@
package im.status.keycard.io;
import android.nfc.tech.IsoDep;
import android.util.Log;
import java.io.IOException;
/**
* Implementation of the CardChannel interface using the Android NFC API.
*/
public class NFCCardChannel implements CardChannel {
private static final String TAG = "CardChannel";
private IsoDep isoDep;
public NFCCardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
byte[] resp = this.isoDep.transceive(apdu);
APDUResponse response = new APDUResponse(resp);
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
return response;
}
@Override
public boolean isConnected() {
return this.isoDep.isConnected();
}
}
@@ -0,0 +1,116 @@
package im.status.keycard.io;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.security.Security;
/**
* Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
* a runloop which monitors the connection and from which CardListener callbacks are called.
*/
public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback {
private static final String TAG = "NFCCardManager";
private static final int DEFAULT_LOOP_SLEEP_MS = 50;
private IsoDep isoDep;
private boolean isRunning;
private CardListener cardListener;
private int loopSleepMS;
/**
* Constructs an NFC Card Manager with default delay between loop iterations.
*/
public NFCCardManager() {
this(DEFAULT_LOOP_SLEEP_MS);
}
/**
* Constructs an NFC Card Manager with the given delay between loop iterations.
*
* @param loopSleepMS time to sleep between loops
*/
public NFCCardManager(int loopSleepMS) {
this.loopSleepMS = loopSleepMS;
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
/**
* True if connected, false otherwise.
* @return if connected, false otherwise
*/
public boolean isConnected() {
return isoDep != null && isoDep.isConnected();
}
@Override
public void onTagDiscovered(Tag tag) {
isoDep = IsoDep.get(tag);
try {
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException e) {
Log.e(TAG, "error connecting to tag");
}
}
/**
* Runloop. Do NOT invoke directly. Use start() instead.
*/
public void run() {
boolean connected = isConnected();
while (true) {
boolean newConnected = isConnected();
if (newConnected != connected) {
connected = newConnected;
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
if (connected && !isRunning) {
onCardConnected();
} else {
onCardDisconnected();
}
}
SystemClock.sleep(loopSleepMS);
}
}
/**
* Reacts on card connected by calling the callback of the registered listener.
*/
private void onCardConnected() {
isRunning = true;
if (cardListener != null) {
cardListener.onConnected(new NFCCardChannel(isoDep));
}
isRunning = false;
}
/**
* Reacts on card disconnected by calling the callback of the registered listener.
*/
private void onCardDisconnected() {
isRunning = false;
isoDep = null;
if (cardListener != null) {
cardListener.onDisconnected();
}
}
/**
* Sets the card listener.
*
* @param listener the new listener
*/
public void setCardListener(CardListener listener) {
cardListener = listener;
}
}