diff --git a/README.md b/README.md index e6cd89e..16d45bd 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/demo/build.gradle b/demo/build.gradle index fcab7b1..f9161b0 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -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 diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index d1d1a3d..9955e6f 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="im.status.keycard.demo"> - + diff --git a/demo/src/main/java/im/status/hardwallet_lite_android/app/MainActivity.java b/demo/src/main/java/im/status/keycard/app/MainActivity.java similarity index 87% rename from demo/src/main/java/im/status/hardwallet_lite_android/app/MainActivity.java rename to demo/src/main/java/im/status/keycard/app/MainActivity.java index b3f23d0..bbe4d9b 100644 --- a/demo/src/main/java/im/status/hardwallet_lite_android/app/MainActivity.java +++ b/demo/src/main/java/im/status/keycard/app/MainActivity.java @@ -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")) { diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml index 44bb785..8952209 100644 --- a/demo/src/main/res/layout/activity_main.xml +++ b/demo/src/main/res/layout/activity_main.xml @@ -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"> - HardwalletLiteAndroid + KeycardAndroid diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index 5610ad9..76746b3 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="im.status.keycard"> diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/ApplicationID.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/ApplicationID.java deleted file mode 100644 index 58a4d70..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/ApplicationID.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Crypto.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Crypto.java deleted file mode 100644 index d68b24e..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Crypto.java +++ /dev/null @@ -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); - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/GlobalPlatformCommandSet.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/GlobalPlatformCommandSet.java deleted file mode 100644 index 7d570f1..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/GlobalPlatformCommandSet.java +++ /dev/null @@ -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); - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Load.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Load.java deleted file mode 100644 index 69df8fc..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Load.java +++ /dev/null @@ -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 files = this.loadFiles(in); - in.close(); - this.fullData = this.getCode(files); - } - - public Map loadFiles(InputStream in) throws IOException { - Map 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 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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Keys.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Keys.java deleted file mode 100644 index 00f3418..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Keys.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Wrapper.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Wrapper.java deleted file mode 100644 index f7bd300..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SCP02Wrapper.java +++ /dev/null @@ -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; - } -} - diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SecureChannel.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SecureChannel.java deleted file mode 100644 index ddd466e..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/SecureChannel.java +++ /dev/null @@ -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); - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Session.java b/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Session.java deleted file mode 100644 index bce5d9e..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/globalplatform/Session.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUCommand.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUCommand.java deleted file mode 100644 index af2b3f1..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUCommand.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUException.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUException.java deleted file mode 100644 index ad764b2..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUException.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUResponse.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUResponse.java deleted file mode 100644 index 99e270b..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/APDUResponse.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardChannel.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/CardChannel.java deleted file mode 100644 index 047ced7..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardChannel.java +++ /dev/null @@ -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(); - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardListener.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/CardListener.java deleted file mode 100644 index 636e2c1..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package im.status.hardwallet_lite_android.io; - -public interface CardListener { - void onConnected(CardChannel channel); - void onDisconnected(); -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardManager.java b/lib/src/main/java/im/status/hardwallet_lite_android/io/CardManager.java deleted file mode 100644 index c2bfacf..0000000 --- a/lib/src/main/java/im/status/hardwallet_lite_android/io/CardManager.java +++ /dev/null @@ -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; - } -} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationInfo.java b/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java similarity index 96% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationInfo.java rename to lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java index d13937b..b29d023 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationInfo.java +++ b/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java @@ -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 */ diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationStatus.java b/lib/src/main/java/im/status/keycard/applet/ApplicationStatus.java similarity index 96% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationStatus.java rename to lib/src/main/java/im/status/keycard/applet/ApplicationStatus.java index 6b5eba9..fe538c5 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/ApplicationStatus.java +++ b/lib/src/main/java/im/status/keycard/applet/ApplicationStatus.java @@ -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. diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/BIP32KeyPair.java b/lib/src/main/java/im/status/keycard/applet/BIP32KeyPair.java similarity index 99% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/BIP32KeyPair.java rename to lib/src/main/java/im/status/keycard/applet/BIP32KeyPair.java index a0c38ed..670bd5a 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/BIP32KeyPair.java +++ b/lib/src/main/java/im/status/keycard/applet/BIP32KeyPair.java @@ -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; diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/CardDuplicator.java b/lib/src/main/java/im/status/keycard/applet/CardDuplicator.java similarity index 80% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/CardDuplicator.java rename to lib/src/main/java/im/status/keycard/applet/CardDuplicator.java index 3e9f6f1..73df6a8 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/CardDuplicator.java +++ b/lib/src/main/java/im/status/keycard/applet/CardDuplicator.java @@ -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(); } diff --git a/lib/src/main/java/im/status/keycard/applet/Identifiers.java b/lib/src/main/java/im/status/keycard/applet/Identifiers.java new file mode 100644 index 0000000..2cd63bf --- /dev/null +++ b/lib/src/main/java/im/status/keycard/applet/Identifiers.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/KeyPath.java b/lib/src/main/java/im/status/keycard/applet/KeyPath.java similarity index 83% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/KeyPath.java rename to lib/src/main/java/im/status/keycard/applet/KeyPath.java index 5415283..31165c0 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/KeyPath.java +++ b/lib/src/main/java/im/status/keycard/applet/KeyPath.java @@ -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; } diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/WalletAppletCommandSet.java b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java similarity index 97% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/WalletAppletCommandSet.java rename to lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java index 412cc75..768265a 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/WalletAppletCommandSet.java +++ b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java @@ -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?"); diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/Mnemonic.java b/lib/src/main/java/im/status/keycard/applet/Mnemonic.java similarity index 86% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/Mnemonic.java rename to lib/src/main/java/im/status/keycard/applet/Mnemonic.java index d7ae4ae..054fa36 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/Mnemonic.java +++ b/lib/src/main/java/im/status/keycard/applet/Mnemonic.java @@ -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(); + } } diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/Pairing.java b/lib/src/main/java/im/status/keycard/applet/Pairing.java similarity index 96% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/Pairing.java rename to lib/src/main/java/im/status/keycard/applet/Pairing.java index 85d2335..2495ab4 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/Pairing.java +++ b/lib/src/main/java/im/status/keycard/applet/Pairing.java @@ -1,4 +1,4 @@ -package im.status.hardwallet_lite_android.wallet; +package im.status.keycard.applet; import org.spongycastle.util.encoders.Base64; diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/RecoverableSignature.java b/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java similarity index 98% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/RecoverableSignature.java rename to lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java index 74a5bbc..36d3b48 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/RecoverableSignature.java +++ b/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java @@ -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; diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/SecureChannelSession.java b/lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java similarity index 98% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/SecureChannelSession.java rename to lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java index e7ccfb3..5a04107 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/SecureChannelSession.java +++ b/lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java @@ -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; diff --git a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/TinyBERTLV.java b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java similarity index 98% rename from lib/src/main/java/im/status/hardwallet_lite_android/wallet/TinyBERTLV.java rename to lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java index 784f12a..01cd33e 100644 --- a/lib/src/main/java/im/status/hardwallet_lite_android/wallet/TinyBERTLV.java +++ b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java @@ -1,4 +1,4 @@ -package im.status.hardwallet_lite_android.wallet; +package im.status.keycard.applet; import java.util.Arrays; diff --git a/lib/src/main/java/im/status/keycard/globalplatform/Crypto.java b/lib/src/main/java/im/status/keycard/globalplatform/Crypto.java new file mode 100644 index 0000000..ce432d8 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/Crypto.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java b/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java new file mode 100644 index 0000000..5534af8 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java @@ -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]); + } +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/Load.java b/lib/src/main/java/im/status/keycard/globalplatform/Load.java new file mode 100644 index 0000000..89a971f --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/Load.java @@ -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 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 loadFiles(InputStream in) throws IOException { + Map 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 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; + } +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/LoadCallback.java b/lib/src/main/java/im/status/keycard/globalplatform/LoadCallback.java new file mode 100644 index 0000000..91fafd9 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/LoadCallback.java @@ -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); +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/SCP02Keys.java b/lib/src/main/java/im/status/keycard/globalplatform/SCP02Keys.java new file mode 100644 index 0000000..de8aa76 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/SCP02Keys.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/SCP02Wrapper.java b/lib/src/main/java/im/status/keycard/globalplatform/SCP02Wrapper.java new file mode 100644 index 0000000..6c5b114 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/SCP02Wrapper.java @@ -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; + } +} + diff --git a/lib/src/main/java/im/status/keycard/globalplatform/SecureChannel.java b/lib/src/main/java/im/status/keycard/globalplatform/SecureChannel.java new file mode 100644 index 0000000..d7d7e07 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/SecureChannel.java @@ -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); + } +} diff --git a/lib/src/main/java/im/status/keycard/globalplatform/Session.java b/lib/src/main/java/im/status/keycard/globalplatform/Session.java new file mode 100644 index 0000000..fc7edef --- /dev/null +++ b/lib/src/main/java/im/status/keycard/globalplatform/Session.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/io/APDUCommand.java b/lib/src/main/java/im/status/keycard/io/APDUCommand.java new file mode 100644 index 0000000..5191b65 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/APDUCommand.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/io/APDUException.java b/lib/src/main/java/im/status/keycard/io/APDUException.java new file mode 100644 index 0000000..0d5bb6b --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/APDUException.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/io/APDUResponse.java b/lib/src/main/java/im/status/keycard/io/APDUResponse.java new file mode 100644 index 0000000..d1f529d --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/APDUResponse.java @@ -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; + } +} diff --git a/lib/src/main/java/im/status/keycard/io/CardChannel.java b/lib/src/main/java/im/status/keycard/io/CardChannel.java new file mode 100644 index 0000000..8f65d9b --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/CardChannel.java @@ -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(); +} diff --git a/lib/src/main/java/im/status/keycard/io/CardListener.java b/lib/src/main/java/im/status/keycard/io/CardListener.java new file mode 100644 index 0000000..f378d7c --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/CardListener.java @@ -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(); +} diff --git a/lib/src/main/java/im/status/keycard/io/NFCCardChannel.java b/lib/src/main/java/im/status/keycard/io/NFCCardChannel.java new file mode 100644 index 0000000..117f45d --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/NFCCardChannel.java @@ -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(); + } +} diff --git a/lib/src/main/java/im/status/keycard/io/NFCCardManager.java b/lib/src/main/java/im/status/keycard/io/NFCCardManager.java new file mode 100644 index 0000000..fd3c51b --- /dev/null +++ b/lib/src/main/java/im/status/keycard/io/NFCCardManager.java @@ -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; + } +}