[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:
@@ -1,8 +1,8 @@
|
||||
# Hardwallet Android SDK
|
||||
# Keycard Android SDK
|
||||
|
||||
This SDK simplifies integration with the [Status Keycard](https://github.com/status-im/hardware-wallet) in Android
|
||||
This SDK simplifies integration with the [Status Keycard](https://github.com/status-im/status-keycard) in Android
|
||||
applications. In this SDK you find both the classes needed for generic communication with SmartCards as well as classes
|
||||
specifically addressing the Keycard.
|
||||
|
||||
To get started, check the file demo/src/main/java/im/status/hardwallet_lite_android/app/MainActivity.java which a simple
|
||||
To get started, check the file demo/src/main/java/im/status/keycard/app/MainActivity.java which a simple
|
||||
demo application showing how the SDK works and what you can do with it.
|
||||
+1
-1
@@ -3,7 +3,7 @@ apply plugin: 'com.android.application'
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
applicationId "im.status.hardwallet_lite_android.demo"
|
||||
applicationId "im.status.keycard.demo"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.status.hardwallet_lite_android.demo">
|
||||
package="im.status.keycard.demo">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -9,7 +9,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name="im.status.hardwallet_lite_android.app.MainActivity">
|
||||
<activity android:name="im.status.keycard.app.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
|
||||
+14
-14
@@ -1,14 +1,14 @@
|
||||
package im.status.hardwallet_lite_android.app;
|
||||
package im.status.keycard.app;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import im.status.hardwallet_lite_android.demo.R;
|
||||
import im.status.hardwallet_lite_android.io.CardChannel;
|
||||
import im.status.hardwallet_lite_android.io.CardListener;
|
||||
import im.status.hardwallet_lite_android.io.CardManager;
|
||||
import im.status.hardwallet_lite_android.wallet.*;
|
||||
import im.status.keycard.demo.R;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.io.CardListener;
|
||||
import im.status.keycard.io.NFCCardManager;
|
||||
import im.status.keycard.applet.*;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
@@ -16,21 +16,21 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private CardManager cardManager;
|
||||
private NFCCardManager cardManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
cardManager = new CardManager();
|
||||
cardManager = new NFCCardManager();
|
||||
|
||||
cardManager.setCardListener(new CardListener() {
|
||||
@Override
|
||||
public void onConnected(CardChannel cardChannel) {
|
||||
try {
|
||||
// Applet-specific code
|
||||
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(cardChannel);
|
||||
KeycardCommandSet cmdSet = new KeycardCommandSet(cardChannel);
|
||||
|
||||
Log.i(TAG, "Applet selection successful");
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
// can be either generated or chosen by the user. Using fixed values is highly discouraged.
|
||||
if (!info.isInitializedCard()) {
|
||||
Log.i(TAG, "Initializing card with test secrets");
|
||||
cmdSet.init("000000", "123456789012", "WalletAppletTest").checkOK();
|
||||
cmdSet.init("000000", "123456789012", "KeycardTest").checkOK();
|
||||
info = new ApplicationInfo(cmdSet.select().checkOK().getData());
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
// In real projects, the pairing key should be saved and used for all new sessions.
|
||||
cmdSet.autoPair("WalletAppletTest");
|
||||
cmdSet.autoPair("KeycardTest");
|
||||
Pairing pairing = cmdSet.getPairing();
|
||||
|
||||
// Never log the pairing key in a real application!
|
||||
@@ -70,7 +70,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
Log.i(TAG, "Secure channel opened. Getting applet status.");
|
||||
|
||||
// We send a GET STATUS command, which does not require PIN authentication
|
||||
ApplicationStatus status = new ApplicationStatus(cmdSet.getStatus(WalletAppletCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
|
||||
ApplicationStatus status = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
|
||||
|
||||
Log.i(TAG, "PIN retry counter: " + status.getPINRetryCount());
|
||||
Log.i(TAG, "PUK retry counter: " + status.getPUKRetryCount());
|
||||
@@ -79,7 +79,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
// A mnemonic can be generated before PIN authentication. Generating a mnemonic does not create keys on the
|
||||
// card. a subsequent loadKey step must be performed after PIN authentication. In this example we will only
|
||||
// show how to convert the output of the card to a usable format but won't actually load the key
|
||||
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(WalletAppletCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
|
||||
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(KeycardCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
|
||||
|
||||
// We need to set a wordlist if we plan using this object to derive the binary seed. If we just need the word
|
||||
// indexes we can skip this step and call mnemonic.getIndexes() instead.
|
||||
@@ -102,7 +102,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
// Get the current key path using GET STATUS
|
||||
KeyPath currentPath = new KeyPath(cmdSet.getStatus(WalletAppletCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData());
|
||||
KeyPath currentPath = new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData());
|
||||
Log.i(TAG, "Current key path: " + currentPath);
|
||||
|
||||
if (!currentPath.toString().equals("m/44'/0'/0'/0/0")) {
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="im.status.hardwallet_lite_android.app.MainActivity">
|
||||
tools:context="im.status.keycard.app.MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">HardwalletLiteAndroid</string>
|
||||
<string name="app_name">KeycardAndroid</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.status.hardwallet_lite_android">
|
||||
package="im.status.keycard">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
-130
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
-1
@@ -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
-1
@@ -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;
|
||||
+9
-9
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -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;
|
||||
}
|
||||
+9
-14
@@ -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?");
|
||||
+26
-4
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package im.status.hardwallet_lite_android.wallet;
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.spongycastle.util.encoders.Base64;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+5
-5
@@ -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
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user