diff --git a/demo-android/.gitignore b/demo-android/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/demo-android/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/demo-android/build.gradle b/demo-android/build.gradle
deleted file mode 100644
index 4660811..0000000
--- a/demo-android/build.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-apply plugin: 'com.android.application'
-
-android {
- compileSdkVersion 28
- defaultConfig {
- applicationId "im.status.keycard.demo"
- minSdkVersion 19
- targetSdkVersion 28
- versionCode 300
- versionName "3.0.0"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-
- lintOptions {
- lintConfig file("lint-config.xml")
- }
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-}
-
-dependencies {
- implementation 'com.android.support:appcompat-v7:28.0.0'
- implementation 'com.android.support.constraint:constraint-layout:1.1.3'
- implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
-
- implementation project(':android')
-
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-}
diff --git a/demo-android/lint-config.xml b/demo-android/lint-config.xml
deleted file mode 100644
index e1957ce..0000000
--- a/demo-android/lint-config.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/demo-android/proguard-rules.pro b/demo-android/proguard-rules.pro
deleted file mode 100644
index f1b4245..0000000
--- a/demo-android/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
diff --git a/demo-android/src/main/AndroidManifest.xml b/demo-android/src/main/AndroidManifest.xml
deleted file mode 100644
index 9955e6f..0000000
--- a/demo-android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/demo-android/src/main/java/im/status/keycard/app/MainActivity.java b/demo-android/src/main/java/im/status/keycard/app/MainActivity.java
deleted file mode 100644
index 82bad57..0000000
--- a/demo-android/src/main/java/im/status/keycard/app/MainActivity.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package im.status.keycard.app;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.nfc.NfcAdapter;
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.util.Log;
-import im.status.keycard.android.LedgerBLEManager;
-import im.status.keycard.demo.R;
-import im.status.keycard.io.CardChannel;
-import im.status.keycard.io.CardListener;
-import im.status.keycard.android.NFCCardManager;
-import im.status.keycard.applet.*;
-import org.bouncycastle.util.encoders.Hex;
-
-public class MainActivity extends AppCompatActivity {
-
- private static final String TAG = "MainActivity";
-
- private NfcAdapter nfcAdapter;
- private NFCCardManager cardManager;
- //private LedgerBLEManager cardManager;
- //private boolean connected;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- nfcAdapter = NfcAdapter.getDefaultAdapter(this);
- cardManager = new NFCCardManager();
- //cardManager = new LedgerBLEManager(this);
- cardManager.setCardListener(new CardListener() {
- @Override
- public void onConnected(CardChannel cardChannel) {
- try {
- // Applet-specific code
- KeycardCommandSet cmdSet = new KeycardCommandSet(cardChannel);
-
- Log.i(TAG, "Applet selection successful");
-
- // First thing to do is selecting the applet on the card.
- ApplicationInfo info = new ApplicationInfo(cmdSet.select().checkOK().getData());
-
- // If the card is not initialized, the INIT apdu must be sent. The actual PIN, PUK and pairing password values
- // 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", "KeycardTest").checkOK();
- info = new ApplicationInfo(cmdSet.select().checkOK().getData());
- }
-
- Log.i(TAG, "Instance UID: " + Hex.toHexString(info.getInstanceUID()));
- Log.i(TAG, "Secure channel public key: " + Hex.toHexString(info.getSecureChannelPubKey()));
- Log.i(TAG, "Application version: " + info.getAppVersionString());
- Log.i(TAG, "Free pairing slots: " + info.getFreePairingSlots());
- if (info.hasMasterKey()) {
- Log.i(TAG, "Key UID: " + Hex.toHexString(info.getKeyUID()));
- } else {
- Log.i(TAG, "The card has no master key");
- }
- Log.i(TAG, String.format("Capabilities: %02X", info.getCapabilities()));
- Log.i(TAG, "Has Secure Channel: " + info.hasSecureChannelCapability());
- Log.i(TAG, "Has Key Management: " + info.hasKeyManagementCapability());
- Log.i(TAG, "Has Credentials Management: " + info.hasCredentialsManagementCapability());
- Log.i(TAG, "Has NDEF capability: " + info.hasNDEFCapability());
-
- if (info.hasSecureChannelCapability()) {
- // In real projects, the pairing key should be saved and used for all new sessions.
- cmdSet.autoPair("KeycardTest");
- Pairing pairing = cmdSet.getPairing();
-
- // Never log the pairing key in a real application!
- Log.i(TAG, "Pairing with card is done.");
- Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
- Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
-
- // Opening a Secure Channel is needed for all other applet commands
- cmdSet.autoOpenSecureChannel();
-
- 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(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
-
- Log.i(TAG, "PIN retry counter: " + status.getPINRetryCount());
- Log.i(TAG, "PUK retry counter: " + status.getPUKRetryCount());
- Log.i(TAG, "Has master key: " + status.hasMasterKey());
-
- if (info.hasKeyManagementCapability()) {
- // 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(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.
- mnemonic.fetchBIP39EnglishWordlist();
-
- Log.i(TAG, "Generated mnemonic phrase: " + mnemonic.toMnemonicPhrase());
- Log.i(TAG, "Binary seed: " + Hex.toHexString(mnemonic.toBinarySeed()));
- }
-
- if (info.hasCredentialsManagementCapability()) {
- // PIN authentication allows execution of privileged commands
- cmdSet.verifyPIN("000000").checkAuthOK();
-
- Log.i(TAG, "Pin Verified.");
- }
-
- // If the card has no keys, we generate a new set. Keys can also be loaded on the card starting from a binary
- // seed generated from a mnemonic phrase. In alternative, we could load the generated keypair as shown in the
- // commented line of code.
- if (!status.hasMasterKey() && info.hasKeyManagementCapability()) {
- cmdSet.generateKey();
- //cmdSet.loadKey(mnemonic.toBIP32KeyPair());
- }
-
- // Get the current key path using GET STATUS
- 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")) {
- // Key derivation is needed to select the desired key. The derived key remains current until a new derive
- // command is sent (it is not lost on power loss).
- cmdSet.deriveKey("m/44'/0'/0'/0/0").checkOK();
- Log.i(TAG, "Derived m/44'/0'/0'/0/0");
- }
-
- // We retrieve the wallet public key
- BIP32KeyPair walletPublicKey = BIP32KeyPair.fromTLV(cmdSet.exportCurrentKey(true).checkOK().getData());
-
- Log.i(TAG, "Wallet public key: " + Hex.toHexString(walletPublicKey.getPublicKey()));
- Log.i(TAG, "Wallet address: " + Hex.toHexString(walletPublicKey.toEthereumAddress()));
-
- byte[] hash = "thiscouldbeahashintheorysoitisok".getBytes();
-
- RecoverableSignature signature = new RecoverableSignature(hash, cmdSet.sign(hash).checkOK().getData());
-
- Log.i(TAG, "Signed hash: " + Hex.toHexString(hash));
- Log.i(TAG, "Recovery ID: " + signature.getRecId());
- Log.i(TAG, "R: " + Hex.toHexString(signature.getR()));
- Log.i(TAG, "S: " + Hex.toHexString(signature.getS()));
-
- if (info.hasSecureChannelCapability()) {
- // Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
- // We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
- // this would be a very bad idea to do.
- cmdSet.unpairOthers();
- cmdSet.autoUnpair();
-
- Log.i(TAG, "Unpaired.");
- }
- } catch (Exception e) {
- Log.e(TAG, e.getMessage());
- }
-
- }
-
- @Override
- public void onDisconnected() {
- Log.i(TAG, "Card disconnected.");
- }
- });
- cardManager.start();
- /*connected = false;
- cardManager.startScan(new BluetoothAdapter.LeScanCallback() {
- @Override
- public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
- if (connected) {
- return;
- }
-
- connected = true;
- cardManager.stopScan(this);
- cardManager.connectDevice(device);
- }
- });*/
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (nfcAdapter != null) {
- nfcAdapter.enableReaderMode(this, this.cardManager, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (nfcAdapter != null) {
- nfcAdapter.disableReaderMode(this);
- }
- }
-}
diff --git a/demo-android/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demo-android/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 6348baa..0000000
--- a/demo-android/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demo-android/src/main/res/drawable/ic_launcher_background.xml b/demo-android/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 867f14a..0000000
--- a/demo-android/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demo-android/src/main/res/layout/activity_main.xml b/demo-android/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 8952209..0000000
--- a/demo-android/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index bbd3e02..0000000
--- a/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index bbd3e02..0000000
--- a/demo-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/demo-android/src/main/res/mipmap-hdpi/ic_launcher.png b/demo-android/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a2f5908..0000000
Binary files a/demo-android/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demo-android/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 1b52399..0000000
Binary files a/demo-android/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-mdpi/ic_launcher.png b/demo-android/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index ff10afd..0000000
Binary files a/demo-android/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demo-android/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 115a4c7..0000000
Binary files a/demo-android/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo-android/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index dcd3cd8..0000000
Binary files a/demo-android/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demo-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 459ca60..0000000
Binary files a/demo-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 8ca12fe..0000000
Binary files a/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 8e19b41..0000000
Binary files a/demo-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index b824ebd..0000000
Binary files a/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index 4c19a13..0000000
Binary files a/demo-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/demo-android/src/main/res/values/colors.xml b/demo-android/src/main/res/values/colors.xml
deleted file mode 100644
index 3ab3e9c..0000000
--- a/demo-android/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- #3F51B5
- #303F9F
- #FF4081
-
diff --git a/demo-android/src/main/res/values/strings.xml b/demo-android/src/main/res/values/strings.xml
deleted file mode 100644
index d7a6510..0000000
--- a/demo-android/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- KeycardAndroid
-
diff --git a/demo-android/src/main/res/values/styles.xml b/demo-android/src/main/res/values/styles.xml
deleted file mode 100644
index 5885930..0000000
--- a/demo-android/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java b/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java
index 9aee41b..f4ad2e0 100644
--- a/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java
+++ b/lib/src/main/java/im/status/keycard/applet/ApplicationInfo.java
@@ -12,12 +12,15 @@ public class ApplicationInfo {
private byte freePairingSlots;
private byte[] keyUID;
private byte capabilities;
+ private byte[] certData;
+ private byte appStatus;
public static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
public static final byte TLV_PUB_KEY = (byte) 0x80;
public static final byte TLV_UID = (byte) 0x8F;
public static final byte TLV_KEY_UID = (byte) 0x8E;
public static final byte TLV_CAPABILITIES = (byte) 0x8D;
+ public static final byte TLV_STATUS = (byte) 0x8C;
static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01;
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
@@ -27,6 +30,9 @@ public class ApplicationInfo {
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF | CAPABILITY_FACTORY_RESET;
+ static final byte APP_STATUS_INITIALIZED = 0x10;
+ static final byte APP_STATUS_LEE_MODE = 0x20;
+
/**
* Constructs an object by parsing the TLV data.
*
@@ -36,10 +42,7 @@ public class ApplicationInfo {
public ApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
- int topTag = tlv.readTag();
- tlv.unreadLastTag();
-
- if (topTag == TLV_PUB_KEY) {
+ if (tlv.nextTagIs(TLV_PUB_KEY)) {
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
initializedCard = false;
capabilities = CAPABILITY_CREDENTIALS_MANAGEMENT;
@@ -52,20 +55,50 @@ public ApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
}
tlv.enterConstructed(TLV_APPLICATION_INFO_TEMPLATE);
- instanceUID = tlv.readPrimitive(TLV_UID);
- secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
+
+ // Parse fields conditionally by tag for cross-version compatibility.
+
+ // instanceUID (0x8F) - present in V1-V3, absent in V4+
+ if (tlv.nextTagIs(TLV_UID)) {
+ instanceUID = tlv.readPrimitive(TLV_UID);
+ }
+
+ // secureChannelPubKey (0x80) - present in V1-V3, absent in V4+
+ if (tlv.nextTagIs(TLV_PUB_KEY)) {
+ secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
+ }
+
+ // appVersion (INTEGER 0x02) - present in all versions
appVersion = (short) tlv.readInt();
- freePairingSlots = (byte) tlv.readInt();
+
+ // appStatud (0x8C) - initialized, lee mode, pin retries
+ if (tlv.nextTagIs(TLV_STATUS)) {
+ appStatus = tlv.readPrimitive(TLV_STATUS)[0];
+ initializedCard = (appStatus & APP_STATUS_INITIALIZED) == APP_STATUS_INITIALIZED;
+ } else {
+ appStatus = APP_STATUS_INITIALIZED;
+ initializedCard = true;
+ }
+
+ // freePairingSlots (INTEGER 0x02) - present in V1-V3, absent in V4+
+ if (tlv.nextTagIs(TinyBERTLV.TLV_INT)) {
+ freePairingSlots = (byte) tlv.readInt();
+ }
+
+ // keyUID (0x8E) - present in all versions
keyUID = tlv.readPrimitive(TLV_KEY_UID);
- if (tlv.readTag() != TinyBERTLV.END_OF_TLV) {
- tlv.unreadLastTag();
+ // capabilities (0x8D) - present in V2+
+ if (tlv.nextTagIs(TLV_CAPABILITIES)) {
capabilities = tlv.readPrimitive(TLV_CAPABILITIES)[0];
} else {
capabilities = CAPABILITIES_ALL;
}
- initializedCard = true;
+ // Parse certificate - present in V4+
+ if (tlv.nextTagIs(Certificate.TLV_CERT)) {
+ certData = tlv.readPrimitive(Certificate.TLV_CERT);
+ }
}
/**
@@ -200,5 +233,40 @@ public boolean hasNDEFCapability() {
*/
public boolean hasFactoryResetCapability() {
return (capabilities & CAPABILITY_FACTORY_RESET) == CAPABILITY_FACTORY_RESET;
- }
+ }
+
+ /**
+ * Returns true if the device has loaded LEE keys.
+ *
+ * @return true or false
+ */
+ public boolean isLEEMode() {
+ return (appStatus & APP_STATUS_LEE_MODE) == APP_STATUS_LEE_MODE;
+ }
+
+ /**
+ * Returns the number of remaining PIN retries.
+ * Only available on applet V4+, returns -1 on older cards.
+ *
+ * @return pin retry counter
+ */
+ public byte getPINRetries() {
+ if (appVersion < 0x0400) {
+ return -1;
+ }
+
+ return (byte) (appStatus & 0x0f);
+ }
+
+ /**
+ * Returns the raw identity certificate data (V2 only).
+ *
+ * This is the 98-byte certificate: compressed_pubkey(33) || r(32) || s(32) || v(1).
+ * Used by {@link SecureChannelV2} for card authentication during handshake.
+ *
+ * @return the certificate data, or null if not present (V1 cards)
+ */
+ public byte[] getCertData() {
+ return certData;
+ }
}
diff --git a/lib/src/main/java/im/status/keycard/applet/Certificate.java b/lib/src/main/java/im/status/keycard/applet/Certificate.java
index 601bbd3..aac98c8 100644
--- a/lib/src/main/java/im/status/keycard/applet/Certificate.java
+++ b/lib/src/main/java/im/status/keycard/applet/Certificate.java
@@ -119,6 +119,15 @@ public static byte[] verifyIdentity(byte[] hash, byte[] tlvData) {
}
}
+ /**
+ * Returns the card's identity public key (compressed, 33 bytes).
+ *
+ * @return the compressed identity public key
+ */
+ public byte[] getIdentPub() {
+ return identPub;
+ }
+
public byte[] toStoreData() {
if (identPriv == null) {
throw new IllegalStateException("The private key must be set.");
diff --git a/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java
index 7f4b12e..3edf88e 100644
--- a/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java
+++ b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java
@@ -10,6 +10,8 @@
import java.io.IOException;
import java.security.KeyPair;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/**
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
@@ -33,8 +35,10 @@ public class KeycardCommandSet {
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
+ static final byte INS_EXPORT_LEE = (byte) 0xC3;
static final byte INS_GET_DATA = (byte) 0xCA;
static final byte INS_STORE_DATA = (byte) 0xE2;
+ static final byte INS_GET_CHALLENGE = (byte) 0x84;
public static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
public static final byte CHANGE_PIN_P1_PUK = 0x01;
@@ -46,6 +50,7 @@ public class KeycardCommandSet {
public static final byte LOAD_KEY_P1_EC = 0x01;
public static final byte LOAD_KEY_P1_EXT_EC = 0x02;
public static final byte LOAD_KEY_P1_SEED = 0x03;
+ public static final byte LOAD_KEY_P1_LEE = 0x04;
public static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
public static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
@@ -57,7 +62,9 @@ public class KeycardCommandSet {
static final byte SIGN_P1_PINLESS = 0x03;
public static final byte SIGN_P2_ECDSA = 0x00;
- public static final byte SIGN_P2_BLS12_381 = 0x01;
+ public static final byte SIGN_P2_EDDSA_ED25519 = 0x01;
+ public static final byte SIGN_P2_BLS12_381 = 0x02;
+ public static final byte SIGN_P2_BIP340_SCHNORR = 0x03;
public static final byte STORE_DATA_P1_PUBLIC = 0x00;
public static final byte STORE_DATA_P1_NDEF = 0x01;
@@ -87,17 +94,67 @@ public class KeycardCommandSet {
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
+ /**
+ * Default Status CA public key (compressed secp256k1, 33 bytes).
+ */
+ public static final byte[] DEFAULT_CA_PUBLIC_KEY = {
+ (byte) 0x02,
+ (byte) 0x9a, (byte) 0xb9, (byte) 0x9e, (byte) 0xe1, (byte) 0xe7, (byte) 0xa7, (byte) 0x1b,
+ (byte) 0xdf, (byte) 0x45, (byte) 0xb3, (byte) 0xf9, (byte) 0xc5, (byte) 0x8c, (byte) 0x99,
+ (byte) 0x86, (byte) 0x6f, (byte) 0xf1, (byte) 0x29, (byte) 0x4d, (byte) 0x2c, (byte) 0x1e,
+ (byte) 0x30, (byte) 0x4e, (byte) 0x22, (byte) 0x8a, (byte) 0x86, (byte) 0xe1, (byte) 0x0c,
+ (byte) 0x33, (byte) 0x43, (byte) 0x50, (byte) 0x1c
+ };
+
private final CardChannel apduChannel;
- private SecureChannelSession secureChannel;
+ private SecureChannel secureChannel;
private ApplicationInfo info;
+ private final List caPublicKeys;
+ private final List whitelistedCardPublicKeys;
/**
- * Creates a KeycardCommandSet using the given APDU Channel
+ * Creates a KeycardCommandSet using the given APDU Channel.
+ * The secure channel version (V1 or V2) is auto-detected based on the applet
+ * version after the first SELECT command.
+ * Uses the default Status CA public key for V2 certificate verification.
+ *
* @param apduChannel APDU channel
*/
public KeycardCommandSet(CardChannel apduChannel) {
+ this(apduChannel, DEFAULT_CA_PUBLIC_KEY);
+ }
+
+ /**
+ * Creates a KeycardCommandSet using the given APDU Channel and a single
+ * CA public key for V2 certificate verification.
+ *
+ * @param apduChannel APDU channel
+ * @param caPublicKey compressed secp256k1 CA public key (33 bytes)
+ */
+ public KeycardCommandSet(CardChannel apduChannel, byte[] caPublicKey) {
+ this(apduChannel, Collections.singletonList(caPublicKey), Collections.emptyList());
+ }
+
+ /**
+ * Creates a KeycardCommandSet using the given APDU Channel with the given
+ * set of trusted CA public keys and optionally whitelisted card identity
+ * public keys for V2 certificate verification.
+ *
+ * @param apduChannel APDU channel
+ * @param caPublicKeys array of compressed secp256k1 CA public keys (33 bytes each), may be empty but not null
+ * @param whitelistedCardPublicKeys array of compressed card identity public keys (33 bytes each), may be empty but not null
+ */
+ public KeycardCommandSet(CardChannel apduChannel, List caPublicKeys, List whitelistedCardPublicKeys) {
+ if (caPublicKeys == null) {
+ throw new IllegalArgumentException("caPublicKeys must not be null");
+ }
+ if (whitelistedCardPublicKeys == null) {
+ throw new IllegalArgumentException("whitelistedCardPublicKeys must not be null");
+ }
this.apduChannel = apduChannel;
- this.secureChannel = new SecureChannelSession();
+ this.caPublicKeys = caPublicKeys;
+ this.whitelistedCardPublicKeys = whitelistedCardPublicKeys;
+ this.secureChannel = new SecureChannelV2(caPublicKeys, whitelistedCardPublicKeys);
}
/**
@@ -111,11 +168,12 @@ public ApplicationInfo getApplicationInfo() {
}
/**
- * Set the SecureChannel object
- * @param secureChannel secure channel
+ * Returns the current secure channel implementation.
+ *
+ * @return the secure channel
*/
- protected void setSecureChannel(SecureChannelSession secureChannel) {
- this.secureChannel = secureChannel;
+ public SecureChannel getSecureChannel() {
+ return secureChannel;
}
/**
@@ -156,13 +214,19 @@ public APDUResponse select(int instanceIdx) throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID(instanceIdx));
APDUResponse resp = apduChannel.send(selectApplet);
-
if (resp.getSw() == 0x9000) {
info = new ApplicationInfo(resp.getData());
if (info.hasSecureChannelCapability()) {
- this.secureChannel.generateSecret(info.getSecureChannelPubKey());
- this.secureChannel.reset();
+ if (isSecureChannelV2(info)) {
+ SecureChannelV2 scV2 = new SecureChannelV2(caPublicKeys, whitelistedCardPublicKeys);
+ scV2.setCardCertificate(info.getCertData());
+ this.secureChannel = scV2;
+ } else {
+ SecureChannelV1 scV1 = new SecureChannelV1();
+ scV1.generateSecret(info.getSecureChannelPubKey());
+ this.secureChannel = scV1;
+ }
}
}
@@ -426,6 +490,19 @@ public APDUResponse loadKey(byte[] seed) throws IOException {
return loadKey(seed, LOAD_KEY_P1_SEED);
}
+ /**
+ * Sends a LOAD KEY APDU. The given seed is sent as-is and the P1 of the command is set to LOAD_KEY_P1_LEE (0x04).
+ * This works on cards which support public key derivation. The loaded keyset is extended and support further
+ * key derivation.
+ *
+ * @param seed the binary seed
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ public APDUResponse loadLEEKey(byte[] seed) throws IOException {
+ return loadKey(seed, LOAD_KEY_P1_LEE);
+ }
+
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that
* the card will not be able to do further key derivation.
@@ -556,11 +633,26 @@ public APDUResponse sign(byte[] hash) throws IOException {
* @throws IOException communication error
*/
public APDUResponse signWithPath(byte[] hash, String path, boolean makeCurrent) throws IOException {
+ return signWithPath(hash, path, SIGN_P2_ECDSA, makeCurrent);
+ }
+
+ /**
+ * Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The key used to sign is given
+ * as a parameter.
+ *
+ * @param hash the hash to sign
+ * @params path the path of the key to use
+ * @oarams algo the signing algorithm
+ * @param makeCurrent ture if the key used to sign should become the current key, false otherwise
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ public APDUResponse signWithPath(byte[] hash, String path, int algo, boolean makeCurrent) throws IOException {
KeyPath keyPath = new KeyPath(path);
byte[] pathData = keyPath.getData();
byte[] data = Arrays.copyOf(hash, hash.length + pathData.length);
System.arraycopy(pathData, 0, data, hash.length, pathData.length);
- return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE));
+ return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE), algo);
}
/**
@@ -585,10 +677,15 @@ public APDUResponse signPinless(byte[] hash) throws IOException {
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, int p1) throws IOException {
- APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x00, data);
+ return sign(data, p1, SIGN_P2_ECDSA);
+ }
+
+ public APDUResponse sign(byte[] data, int p1, int p2) throws IOException {
+ APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, p2, data);
return secureChannel.transmit(apduChannel, sign);
}
+
/**
* Sends a DERIVE KEY APDU with the given key path.
*
@@ -772,6 +869,31 @@ public APDUResponse exportKey(int derivationOptions, byte p2, byte[] keypath) th
return secureChannel.transmit(apduChannel, exportKey);
}
+ /**
+ * Sends an EXPORT LEE APDU.
+ *
+ * @param keypath the derivation path
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ public APDUResponse exportLEEKey(String keyPath) throws IOException {
+ KeyPath path = new KeyPath(keyPath);
+ return exportLEEKey(path.getData(), path.getSource());
+ }
+
+ /**
+ * Sends an EXPORT LEE APDU.
+ *
+ * @param path the derivation path
+ * @param source the derivation source
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ public APDUResponse exportLEEKey(byte[] path, int source) throws IOException {
+ APDUCommand exportLee = secureChannel.protectedCommand(0x80, INS_EXPORT_LEE, source, 0, path);
+ return secureChannel.transmit(apduChannel, exportLee);
+ }
+
/**
* Sends a GET DATA APDU.
*
@@ -784,6 +906,11 @@ public APDUResponse getData(byte dataType) throws IOException {
return secureChannel.transmit(apduChannel, getData);
}
+ public APDUResponse getChallenge(int len) throws IOException {
+ APDUCommand getChallenge = secureChannel.protectedCommand(0x80, INS_GET_CHALLENGE, len, 0, new byte[0]);
+ return secureChannel.transmit(apduChannel, getChallenge);
+ }
+
/**
* Sends a STORE DATA APDU for NDEF.
*
@@ -860,7 +987,7 @@ public APDUResponse storeData(byte[] data, byte dataType, short off) throws IOEx
* @return the raw card response
* @throws IOException communication error
*/
- public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
+ public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException, APDUException {
return this.init(pin, puk, pairingPassword, (byte) 0, (byte) 0);
}
@@ -875,7 +1002,7 @@ public APDUResponse init(String pin, String puk, String pairingPassword) throws
* @return the raw card response
* @throws IOException communication error
*/
- public APDUResponse init(String pin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
+ public APDUResponse init(String pin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException, APDUException {
return this.init(pin, null, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
@@ -891,7 +1018,7 @@ public APDUResponse init(String pin, String puk, String pairingPassword, byte pi
* @return the raw card response
* @throws IOException communication error
*/
- public APDUResponse init(String pin, String altPin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
+ public APDUResponse init(String pin, String altPin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException, APDUException {
return this.init(pin, altPin, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
@@ -904,7 +1031,7 @@ public APDUResponse init(String pin, String altPin, String puk, String pairingPa
* @return the raw card response
* @throws IOException communication error
*/
- public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
+ public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException, APDUException {
return init(pin, null, puk, sharedSecret, (byte) 0, (byte) 0);
}
@@ -912,15 +1039,16 @@ public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOE
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
- * @param pin the alternative
+ * @param altPin the alternative PIN (may be null)
* @param puk the PUK
- * @param sharedSecret the shared secret for pairing
+ * @param sharedSecret the shared secret for pairing (ignored for V2)
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
- public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException {
+ public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException, APDUException {
+ // Build init data: PIN + PUK [+ retries(2) [+ altPIN]]
int baselen = pin.length() + puk.length() + sharedSecret.length;
int extlen;
@@ -931,7 +1059,7 @@ public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSec
} else {
extlen = 0;
}
-
+
byte[] initData = Arrays.copyOf(pin.getBytes(), baselen + extlen);
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
@@ -940,13 +1068,21 @@ public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSec
initData[baselen] = pinRetries;
initData[baselen + 1] = pukRetries;
- if (extlen > 2) {
+ if (extlen > 2 && altPin != null) {
System.arraycopy(altPin.getBytes(), 0, initData, baselen + 2, altPin.length());
}
}
- APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
- return apduChannel.send(init);
+ if (secureChannel instanceof SecureChannelV2) {
+ // V2: open secure channel first, then send INIT as a normal encrypted command
+ secureChannel.autoOpenSecureChannel(apduChannel);
+ APDUCommand initCmd = secureChannel.protectedCommand(0x80, INS_INIT, 0, 0, initData);
+ return secureChannel.transmit(apduChannel, initCmd);
+ } else {
+ // V1: use one-shot encryption with the static shared secret
+ APDUCommand initCmd = new APDUCommand(0x80, INS_INIT, 0, 0, ((SecureChannelV1) secureChannel).oneShotEncrypt(initData));
+ return apduChannel.send(initCmd);
+ }
}
/**
@@ -958,5 +1094,15 @@ public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSec
public APDUResponse factoryReset() throws IOException {
APDUCommand factoryReset = new APDUCommand(0x80, INS_FACTORY_RESET, FACTORY_RESET_P1_MAGIC, FACTORY_RESET_P2_MAGIC, new byte[0]);
return apduChannel.send(factoryReset);
- }
+ }
+
+ /**
+ * Returns true if the applet uses Secure Channel V2 (app version >= 4.0).
+ *
+ * @param appInfo the application info from SELECT
+ * @return true for Secure Channel V2, false for V1
+ */
+ private boolean isSecureChannelV2(ApplicationInfo appInfo) {
+ return appInfo.getAppVersion() >= 0x0400;
+ }
}
diff --git a/lib/src/main/java/im/status/keycard/applet/SecureChannel.java b/lib/src/main/java/im/status/keycard/applet/SecureChannel.java
new file mode 100644
index 0000000..b1b112f
--- /dev/null
+++ b/lib/src/main/java/im/status/keycard/applet/SecureChannel.java
@@ -0,0 +1,164 @@
+package im.status.keycard.applet;
+
+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 java.io.IOException;
+
+/**
+ * Common interface for Secure Channel implementations.
+ *
+ * V1 (SecureChannelSession): AES-CBC with CMAC, pairing-based key derivation.
+ * V2 (SecureChannelV2Client): ECDHE on secp256k1, HKDF-SHA256, AES-128-CCM.
+ */
+public interface SecureChannel {
+
+ /**
+ * Establishes a Secure Channel with the card, performing the full handshake
+ * including mutual authentication (V1) or card authentication (V2).
+ *
+ * @param apduChannel the APDU channel
+ * @throws IOException communication error
+ * @throws APDUException secure channel error
+ */
+ void autoOpenSecureChannel(CardChannel apduChannel) throws IOException, APDUException;
+
+ /**
+ * Performs the pairing procedure (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @param pairingMode the pairing mode
+ * @param sharedSecret the shared secret
+ * @throws IOException communication error
+ * @throws APDUException pairing error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ void autoPair(CardChannel apduChannel, byte pairingMode, byte[] sharedSecret)
+ throws IOException, APDUException;
+
+ /**
+ * Unpairs the current paired key (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @throws IOException communication error
+ * @throws APDUException unpairing error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ void autoUnpair(CardChannel apduChannel) throws IOException, APDUException;
+
+ /**
+ * Unpair all other clients (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @throws IOException communication error
+ * @throws APDUException unpairing error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ void unpairOthers(CardChannel apduChannel) throws IOException, APDUException;
+
+ /**
+ * Sends an OPEN SECURE CHANNEL APDU.
+ *
+ * @param apduChannel the APDU channel
+ * @param index the P1 parameter (pairing index for V1, ignored for V2)
+ * @param data the data
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data)
+ throws IOException;
+
+ /**
+ * Sends a MUTUALLY AUTHENTICATE APDU (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @return the raw card response
+ * @throws IOException communication error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException;
+
+ /**
+ * Sends a MUTUALLY AUTHENTICATE APDU (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @param data the data
+ * @return the raw card response
+ * @throws IOException communication error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws IOException;
+
+ /**
+ * Sends a PAIR APDU (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @param p1 the P1 parameter
+ * @param p2 the P2 parameter
+ * @param data the data
+ * @return the raw card response
+ * @throws IOException communication error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ APDUResponse pair(CardChannel apduChannel, byte p1, byte p2, byte[] data) throws IOException;
+
+ /**
+ * Sends an UNPAIR APDU (V1 only).
+ *
+ * @param apduChannel the APDU channel
+ * @param p1 the P1 parameter
+ * @return the raw card response
+ * @throws IOException communication error
+ * @throws UnsupportedOperationException if called on a V2 channel
+ */
+ APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException;
+
+ /**
+ * Returns a command APDU with the secure channel wrapper applied.
+ *
+ * For V1: encrypts the data with AES-CBC and prepends the IV.
+ * For V2: wraps the full inner APDU in the secured command format (CLA=0x80, INS=0x18).
+ *
+ * @param cla the CLA byte of the inner command
+ * @param ins the INS byte of the inner command
+ * @param p1 the P1 byte of the inner command
+ * @param p2 the P2 byte of the inner command
+ * @param data the data of the inner command
+ * @return the wrapped command APDU
+ */
+ APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data);
+
+ /**
+ * Transmits a protected command APDU and unwraps the response.
+ *
+ * For V1: verifies the MAC, decrypts the data, extracts the inner SW.
+ * For V2: decrypts the AES-CCM payload, extracts the inner APDU data and SW.
+ *
+ * @param apduChannel the APDU channel
+ * @param apdu the APDU to send
+ * @return the unwrapped response APDU (data only, SW from inner payload)
+ * @throws IOException transmission error
+ */
+ APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException;
+
+ /**
+ * Returns the current pairing data (V1 only).
+ *
+ * @return the pairing, or null for V2
+ */
+ Pairing getPairing();
+
+ /**
+ * Sets the pairing data (V1 only). No-op for V2.
+ *
+ * @param pairing the pairing data
+ */
+ void setPairing(Pairing pairing);
+
+ /**
+ * Resets the secure channel, invalidating the current session.
+ */
+ void reset();
+}
diff --git a/lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java b/lib/src/main/java/im/status/keycard/applet/SecureChannelV1.java
similarity index 97%
rename from lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java
rename to lib/src/main/java/im/status/keycard/applet/SecureChannelV1.java
index adaa4d8..3e5d5b9 100644
--- a/lib/src/main/java/im/status/keycard/applet/SecureChannelSession.java
+++ b/lib/src/main/java/im/status/keycard/applet/SecureChannelV1.java
@@ -21,9 +21,14 @@
import java.util.Arrays;
/**
- * Handles a SecureChannel session with the card.
+ * Handles a SecureChannel V1 session with the card.
+ *
+ * Uses AES-128-CBC with CMAC for encryption and authentication,
+ * with pairing-based key derivation via ECDH on secp256k1.
+ *
+ * Implements the {@link SecureChannel} interface for interoperability with V2.
*/
-public class SecureChannelSession {
+public class SecureChannelV1 implements SecureChannel {
public static final short SC_SECRET_LENGTH = 32;
public static final short SC_BLOCK_SIZE = 16;
@@ -54,7 +59,7 @@ public class SecureChannelSession {
/**
* Constructs a SecureChannel session on the client.
*/
- public SecureChannelSession() {
+ public SecureChannelV1() {
random = new SecureRandom();
open = false;
}
diff --git a/lib/src/main/java/im/status/keycard/applet/SecureChannelV2.java b/lib/src/main/java/im/status/keycard/applet/SecureChannelV2.java
new file mode 100644
index 0000000..96a68c2
--- /dev/null
+++ b/lib/src/main/java/im/status/keycard/applet/SecureChannelV2.java
@@ -0,0 +1,518 @@
+package im.status.keycard.applet;
+
+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.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.interfaces.ECPublicKey;
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import org.bouncycastle.jce.spec.ECPublicKeySpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyAgreement;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.spec.ECGenParameterSpec;
+import java.util.Collections;
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * Client-side implementation of the Secure Channel Protocol v2 (AES-CCM variant).
+ *
+ * Protocol flow:
+ *
+ * - Handshake: ECDHE on secp256k1 + HKDF-SHA256 key derivation
+ * - Card authentication: ECDSA-SHA256 signature over the full key exchange transcript
+ * - Encrypted commands: AES-128-CCM (T=8, L=13) with implicit per-session nonce counter
+ *
+ *
+ * All traffic after the handshake is wrapped in a single command type:
+ * {@code [0x80 | 0x18 | 0x00 | 0x00 | LC'] || ciphertext || tag(8B)}
+ *
+ * The inner APDU (including CLA, INS, P1, P2, LC, and data) is fully encrypted.
+ * The ISO-level SW is always 0x9000 (decrypt OK) or 0x6982 (decrypt error);
+ * the real command SW is inside the encrypted response payload.
+ *
+ * Unlike V1, V2 does not use pairing. Each session establishes independent keys
+ * via ECDHE, authenticated by the card's persistent identity key certificate.
+ */
+public class SecureChannelV2 implements SecureChannel {
+
+ // Protocol constants
+ private static final byte[] PROTOCOL_LABEL = { 's', 'c', '_', 'v', '2', '_', 'c', 'c', 'm' };
+
+ static final short HKDF_SALT_SIZE = 32;
+ static final short PUBKEY_SIZE = 65; // uncompressed secp256k1 point
+ static final short ECDH_SHARED_X_SIZE = 32;
+ static final short OKM_SIZE = 32;
+ static final short AES_KEY_SIZE = 16;
+ static final short CCM_TAG_SIZE = 8;
+ static final short CCM_NONCE_SIZE = 13;
+ static final short SIGNATURE_DOMAIN_LEN = 5;
+
+ static final byte INS_OPEN_SECURE_CHANNEL = (byte) 0x10;
+ static final byte INS_SECURED_APDU = (byte) 0x18;
+
+ // Trusted CA public keys for certificate verification (compressed, 33 bytes each)
+ private final List caPublicKeys;
+ // Whitelisted card identity public keys (compressed, 33 bytes each)
+ // Allows accepting specific cards even when their CA is not trusted
+ private final List whitelistedCardPublicKeys;
+
+ // Session state
+ private SecretKeySpec keyH2C;
+ private SecretKeySpec keyC2H;
+ private Cipher aesCCM;
+ private byte[] nonceCounter;
+ private boolean open;
+
+ // Card identity (set during handshake)
+ private byte[] cardIdentPub; // compressed, 33 bytes
+
+ // Handshake ephemeral key (kept for debug/testing)
+ private byte[] clientEphPub;
+
+ private final SecureRandom random;
+
+ /**
+ * Creates a V2 secure channel client with the given set of trusted CA public keys
+ * and optionally whitelisted card identity public keys.
+ *
+ * During certificate verification, a card is accepted if either:
+ *
+ * - The CA public key recovered from its certificate matches one of the trusted CA keys, or
+ * - The card's identity public key is in the whitelist
+ *
+ *
+ * @param caPublicKeys list of compressed secp256k1 CA public keys (33 bytes each)
+ * @param whitelistedCardPublicKeys list of compressed card identity public keys (33 bytes each)
+ */
+ public SecureChannelV2(List caPublicKeys, List whitelistedCardPublicKeys) {
+ if (caPublicKeys == null) {
+ throw new IllegalArgumentException("caPublicKeys must not be null");
+ }
+ if (whitelistedCardPublicKeys == null) {
+ throw new IllegalArgumentException("whitelistedCardPublicKeys must not be null");
+ }
+ this.caPublicKeys = Collections.unmodifiableList(caPublicKeys);
+ this.whitelistedCardPublicKeys = Collections.unmodifiableList(whitelistedCardPublicKeys);
+ this.random = new SecureRandom();
+ initCiphers();
+ }
+
+ private void initCiphers() {
+ try {
+ aesCCM = Cipher.getInstance("AES/CCM/NoPadding", "BC");
+ nonceCounter = new byte[CCM_NONCE_SIZE];
+ } catch (Exception e) {
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ }
+
+ // ── SecureChannel interface implementation ──────────────────────────
+
+ @Override
+ public void autoOpenSecureChannel(CardChannel apduChannel)
+ throws IOException, APDUException {
+ byte[] salt = new byte[HKDF_SALT_SIZE];
+ random.nextBytes(salt);
+
+ // Generate client ephemeral key pair
+ KeyPair ephKeyPair = generateEphemeralKeyPair();
+ clientEphPub = getUncompressedPublicKey(ephKeyPair.getPublic());
+
+ // Build request: hkdf_salt || client_eph_pub (uncompressed)
+ byte[] requestData = new byte[(int) (HKDF_SALT_SIZE + PUBKEY_SIZE)];
+ System.arraycopy(salt, 0, requestData, 0, HKDF_SALT_SIZE);
+ System.arraycopy(clientEphPub, 0, requestData, HKDF_SALT_SIZE, PUBKEY_SIZE);
+
+ // Send OPEN_SECURE_CHANNEL
+ APDUCommand cmd = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, 0, 0, requestData);
+ APDUResponse resp = apduChannel.send(cmd);
+ resp.checkOK("OPEN SECURE CHANNEL failed");
+
+ processHandshakeResponse(salt, ephKeyPair.getPrivate(), resp.getData());
+ }
+
+ @Override
+ public void autoPair(CardChannel apduChannel, byte pairingMode, byte[] sharedSecret)
+ throws IOException, APDUException {
+ throw new UnsupportedOperationException("Pairing is not supported in Secure Channel V2");
+ }
+
+ @Override
+ public void autoUnpair(CardChannel apduChannel) throws IOException, APDUException {
+ throw new UnsupportedOperationException("Unpairing is not supported in Secure Channel V2");
+ }
+
+ @Override
+ public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
+ throw new UnsupportedOperationException("Unpairing is not supported in Secure Channel V2");
+ }
+
+ @Override
+ public APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data)
+ throws IOException {
+ open = false;
+ APDUCommand cmd = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, 0, 0, data);
+ return apduChannel.send(cmd);
+ }
+
+ @Override
+ public APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException {
+ throw new UnsupportedOperationException(
+ "Mutual authentication is not a separate step in Secure Channel V2");
+ }
+
+ @Override
+ public APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data)
+ throws IOException {
+ throw new UnsupportedOperationException(
+ "Mutual authentication is not a separate step in Secure Channel V2");
+ }
+
+ @Override
+ public APDUResponse pair(CardChannel apduChannel, byte p1, byte p2, byte[] data)
+ throws IOException {
+ throw new UnsupportedOperationException("Pairing is not supported in Secure Channel V2");
+ }
+
+ @Override
+ public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException {
+ throw new UnsupportedOperationException("Unpairing is not supported in Secure Channel V2");
+ }
+
+ @Override
+ public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
+ if (!open) {
+ return new APDUCommand(cla, ins, p1, p2, data);
+ }
+
+ // Build inner APDU: CLA | INS | P1 | P2 | LC | data
+ ByteArrayOutputStream inner = new ByteArrayOutputStream();
+ inner.write(cla & 0xFF);
+ inner.write(ins & 0xFF);
+ inner.write(p1 & 0xFF);
+ inner.write(p2 & 0xFF);
+ inner.write(data.length & 0xFF);
+ inner.write(data, 0, data.length);
+
+ byte[] innerApdu = inner.toByteArray();
+ byte[] ciphertext = encryptCCM(innerApdu);
+
+ return new APDUCommand(0x80, INS_SECURED_APDU, 0, 0, ciphertext);
+ }
+
+ @Override
+ public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException {
+ APDUResponse resp = apduChannel.send(apdu);
+
+ if (resp.getSw() != 0x9000) {
+ open = false;
+ return resp;
+ }
+
+ if (!open) {
+ return resp;
+ }
+
+ byte[] ciphertext = resp.getData();
+ byte[] plaintext = decryptCCM(ciphertext);
+
+ incrementNonce();
+
+ return new APDUResponse(plaintext);
+ }
+
+ @Override
+ public Pairing getPairing() {
+ return null; // V2 does not use pairing
+ }
+
+ @Override
+ public void setPairing(Pairing pairing) {
+ // No-op: V2 does not use pairing
+ }
+
+ @Override
+ public void reset() {
+ open = false;
+ keyH2C = null;
+ keyC2H = null;
+ Arrays.fill(nonceCounter, (byte) 0);
+ cardIdentPub = null;
+ clientEphPub = null;
+ }
+
+ // ── Handshake processing ────────────────────────────────────────────
+
+ /**
+ * Processes the card's handshake response.
+ *
+ * @param salt the HKDF salt sent by the client
+ * @param clientEphPriv the client's ephemeral private key
+ * @param cardResponse the raw response data (card_eph_pub || DER_signature)
+ */
+ void processHandshakeResponse(byte[] salt, PrivateKey clientEphPriv, byte[] cardResponse) throws APDUException {
+ // Parse card response: card_eph_pub (65B) || sig (DER, variable)
+ if (cardResponse.length < PUBKEY_SIZE + 2) {
+ throw new APDUException("Invalid handshake response: too short");
+ }
+
+ byte[] cardEphPub = Arrays.copyOfRange(cardResponse, 0, (int) PUBKEY_SIZE);
+ byte[] signature = Arrays.copyOfRange(cardResponse, (int) PUBKEY_SIZE, cardResponse.length);
+
+ // ECDH key agreement
+ byte[] sharedSecret = computeECDH(clientEphPriv, cardEphPub);
+
+ // HKDF-SHA256 key derivation
+ byte[] okm = hkdfExpand(salt, sharedSecret, PROTOCOL_LABEL, OKM_SIZE);
+
+ // Set session keys: key_h2c = OKM[0..15], key_c2h = OKM[16..31]
+ keyH2C = new SecretKeySpec(okm, 0, AES_KEY_SIZE, "AES");
+ keyC2H = new SecretKeySpec(okm, AES_KEY_SIZE, AES_KEY_SIZE, "AES");
+
+ initCiphers();
+
+ // Verify card's ECDSA signature over transcript
+ // transcript = hkdf_salt || client_eph_pub || card_eph_pub
+ verifyCardSignature(salt, clientEphPub, cardEphPub, signature);
+
+ // Initialize nonce counter to zero
+ Arrays.fill(nonceCounter, (byte) 0);
+ open = true;
+ }
+
+ private void verifyCardSignature(byte[] salt, byte[] clientPub, byte[] cardPub, byte[] signature) throws APDUException {
+ try {
+ // Hash the transcript
+ MessageDigest md = MessageDigest.getInstance("SHA-256", "BC");
+ md.update(PROTOCOL_LABEL);
+ md.update(salt);
+ md.update(clientPub);
+ md.update(cardPub);
+ byte[] transcriptHash = md.digest();
+
+ if (cardIdentPub == null) {
+ throw new APDUException("Card identity public key not available");
+ }
+
+ // Verify the ECDSA signature using standard JCA API
+ Signature verifier = Signature.getInstance("NONEwithECDSA", "BC");
+ ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
+ ECPublicKeySpec keySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(cardIdentPub), ecSpec);
+ ECPublicKey identKey = (ECPublicKey) KeyFactory.getInstance("EC", "BC").generatePublic(keySpec);
+ verifier.initVerify(identKey);
+ verifier.update(transcriptHash);
+
+ if (!verifier.verify(signature)) {
+ throw new APDUException("Card authentication failed: invalid signature");
+ }
+ } catch (APDUException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new APDUException("Card authentication failed: " + e.getMessage());
+ }
+ }
+
+ // ── Certificate handling ────────────────────────────────────────────
+
+ /**
+ * Parses the card's identity certificate from the SELECT response and
+ * validates the CA public key against the known anchor.
+ *
+ * @param certData the 98-byte certificate from the SELECT response
+ * @throws IOException if CA verification fails
+ */
+ public void setCardCertificate(byte[] certData) throws IOException {
+ try {
+ Certificate cert = Certificate.fromTLV(certData);
+ cardIdentPub = cert.getIdentPub();
+
+ // Check if the card's identity public key is whitelisted
+ boolean whitelisted = isCardWhitelisted(cardIdentPub);
+
+ // Check if the CA public key is trusted
+ byte[] caPub = cert.getPublicKey(); // recovered CA public key (compressed)
+ boolean caTrusted = isCaTrusted(caPub);
+
+ if (!caTrusted && !whitelisted) {
+ throw new IOException("Card certificate verification failed: unknown CA public key and card not whitelisted");
+ }
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IOException("Failed to parse card certificate: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Returns the card's identity public key (set during handshake).
+ *
+ * @return compressed public key, or null if not yet set
+ */
+ public byte[] getCardIdentPub() {
+ return cardIdentPub;
+ }
+
+ /**
+ * Checks if the given card identity public key is in the whitelist.
+ *
+ * @param identPub compressed card identity public key (33 bytes)
+ * @return true if the key is whitelisted
+ */
+ public boolean isCardWhitelisted(byte[] identPub) {
+ for (byte[] key : whitelistedCardPublicKeys) {
+ if (Arrays.equals(key, identPub)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the given CA public key is trusted.
+ *
+ * @param caPub compressed CA public key (33 bytes)
+ * @return true if the key is trusted
+ */
+ public boolean isCaTrusted(byte[] caPub) {
+ for (byte[] key : caPublicKeys) {
+ if (Arrays.equals(key, caPub)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // ── Cryptographic helpers ───────────────────────────────────────────
+
+ private KeyPair generateEphemeralKeyPair() {
+ try {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA", "BC");
+ kpg.initialize(new ECGenParameterSpec("secp256k1"), random);
+ return kpg.generateKeyPair();
+ } catch (Exception e) {
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ }
+
+ private byte[] getUncompressedPublicKey(PublicKey pubKey) {
+ return ((ECPublicKey) pubKey).getQ().getEncoded(false);
+ }
+
+ private byte[] computeECDH(PrivateKey clientPriv, byte[] cardPubUncompressed) {
+ try {
+ ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
+
+ ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(cardPubUncompressed), ecSpec);
+ ECPublicKey cardPub = (ECPublicKey) KeyFactory.getInstance("EC", "BC").generatePublic(cardKeySpec);
+
+ KeyAgreement ka = KeyAgreement.getInstance("ECDH", "BC");
+ ka.init(clientPriv);
+ ka.doPhase(cardPub, true);
+ return ka.generateSecret();
+ } catch (Exception e) {
+ throw new RuntimeException("ECDH key agreement failed", e);
+ }
+ }
+
+ /**
+ * HKDF-SHA256 (Extract-then-Expand) as defined in RFC 5869.
+ * Implements the N=1 case used by the protocol.
+ *
+ * @param salt the salt (32 bytes)
+ * @param ikm the input keying material (shared secret)
+ * @param info the context and application specific information
+ * @param length the length of the output keying material
+ * @return the derived key material
+ */
+ private byte[] hkdfExpand(byte[] salt, byte[] ikm, byte[] info, short length) {
+ try {
+ // Extract: PRK = HMAC-SHA256(salt, IKM)
+ javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(salt, "HmacSHA256"));
+ byte[] prk = mac.doFinal(ikm);
+
+ // Expand: OKM = HKDF-Expand(PRK, info, L)
+ // T(0) = empty string (no input to MAC)
+ // T(1) = HMAC-Hash(PRK, T(0) || info || 0x01)
+ mac = javax.crypto.Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(prk, "HmacSHA256"));
+ mac.update(info);
+ mac.update((byte) 0x01);
+ return mac.doFinal();
+ } catch (Exception e) {
+ throw new RuntimeException("HKDF failed", e);
+ }
+ }
+
+ /**
+ * Encrypts plaintext with AES-128-CCM using the client-to-card key.
+ *
+ * @param plaintext the data to encrypt
+ * @return ciphertext with appended 8-byte tag
+ */
+ private byte[] encryptCCM(byte[] plaintext) {
+ try {
+ byte[] nonce = Arrays.copyOf(nonceCounter, CCM_NONCE_SIZE);
+ GCMParameterSpec spec = new GCMParameterSpec(CCM_TAG_SIZE * 8, nonce);
+
+ aesCCM.init(Cipher.ENCRYPT_MODE, keyH2C, spec);
+ byte[] ciphertext = aesCCM.doFinal(plaintext);
+
+ return ciphertext; // CCM appends tag automatically
+ } catch (Exception e) {
+ throw new RuntimeException("AES-CCM encryption failed", e);
+ }
+ }
+
+ /**
+ * Decrypts ciphertext with AES-128-CCM using the card-to-client key.
+ *
+ * @param ciphertext the data to decrypt (includes appended 8-byte tag)
+ * @return the decrypted plaintext
+ */
+ private byte[] decryptCCM(byte[] ciphertext) {
+ try {
+ byte[] nonce = Arrays.copyOf(nonceCounter, CCM_NONCE_SIZE);
+ GCMParameterSpec spec = new GCMParameterSpec(CCM_TAG_SIZE * 8, nonce);
+
+ aesCCM.init(Cipher.DECRYPT_MODE, keyC2H, spec);
+ return aesCCM.doFinal(ciphertext);
+ } catch (Exception e) {
+ open = false;
+ throw new RuntimeException("AES-CCM decryption failed", e);
+ }
+ }
+
+ /**
+ * Increments the 13-byte nonce counter as a big-endian integer.
+ *
+ * @throws RuntimeException on overflow (2^104 would require session reset)
+ */
+ private void incrementNonce() {
+ for (int i = CCM_NONCE_SIZE - 1; i >= 0; i--) {
+ nonceCounter[i]++;
+ if (nonceCounter[i] != 0) {
+ return;
+ }
+ }
+ // Overflow — session must be reset
+ open = false;
+ throw new RuntimeException("Nonce counter overflow — secure channel session expired");
+ }
+}
diff --git a/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java
index 66a480f..84f7d04 100644
--- a/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java
+++ b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java
@@ -147,6 +147,21 @@ public int readTag() {
return (pos < buffer.length) ? buffer[pos++] : END_OF_TLV;
}
+ /**
+ * Checks the next tag. The current implementation only reads tags on one byte. Can be extended if needed.
+ *
+ * @return the tag
+ */
+ public boolean nextTagIs(int expectedTag) {
+ int nextTag = readTag();
+
+ if (nextTag != END_OF_TLV) {
+ unreadLastTag();
+ }
+
+ return nextTag == expectedTag;
+ }
+
/**
* Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed.
*
diff --git a/lib/src/main/java/im/status/keycard/io/LedgerUtil.java b/lib/src/main/java/im/status/keycard/io/LedgerUtil.java
deleted file mode 100644
index 8def964..0000000
--- a/lib/src/main/java/im/status/keycard/io/LedgerUtil.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package im.status.keycard.io;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-public class LedgerUtil {
- private static final int LEDGER_DEFAULT_CHANNEL = 1;
- private static final int TAG_APDU = 0x05;
-
- private LedgerUtil() {}
-
- public interface Callback {
- void write(byte[] chunk) throws IOException;
- void read(byte[] chunk) throws IOException;
- }
-
- public static APDUResponse send(APDUCommand cmd, int segmentSize, boolean channelInfo, LedgerUtil.Callback cb) throws IOException {
- int offset = 0;
-
- byte[] command = LedgerUtil.wrapCommandAPDU(cmd.serialize(), segmentSize, channelInfo);
- byte[] chunk = new byte[segmentSize];
-
- while(offset != command.length) {
- System.arraycopy(command, offset, chunk, 0, segmentSize);
- cb.write(chunk);
- offset += segmentSize;
- }
-
- ByteArrayOutputStream response = new ByteArrayOutputStream();
- byte[] responseData = null;
-
- while ((responseData = LedgerUtil.unwrapResponseAPDU(response.toByteArray(), segmentSize, channelInfo)) == null) {
- cb.read(chunk);
- response.write(chunk, 0, segmentSize);
- }
-
- return new APDUResponse(responseData);
- }
-
- private static byte[] unwrapResponseAPDU(byte[] data, int segmentSize, boolean channelInfo) throws IOException {
- if ((data == null) || (data.length < 7 + 5)) {
- return null;
- }
-
- int sequenceIdx = 0;
- int offset = checkResponseHeader(data, 0, sequenceIdx, channelInfo);
-
- int responseLength = ((data[offset++] & 0xff) << 8);
- responseLength |= (data[offset++] & 0xff);
-
- if (data.length < 7 + responseLength) {
- return null;
- }
-
- ByteArrayOutputStream response = new ByteArrayOutputStream();
-
- int headerSize = channelInfo ? 5 : 3;
- int initialHeaderSize = headerSize + 2;
-
- int blockSize = (responseLength > segmentSize - initialHeaderSize ? segmentSize - initialHeaderSize : responseLength);
- response.write(data, offset, blockSize);
- offset += blockSize;
-
- while (response.size() != responseLength) {
- sequenceIdx++;
-
- if (offset == data.length) {
- return null;
- }
-
- offset = checkResponseHeader(data, offset, sequenceIdx, channelInfo);
-
- blockSize = (responseLength - response.size() > segmentSize - headerSize ? segmentSize - headerSize : responseLength - response.size());
- if (blockSize > data.length - offset) {
- return null;
- }
- response.write(data, offset, blockSize);
- offset += blockSize;
- }
-
- return response.toByteArray();
- }
-
- private static int checkResponseHeader(byte[] data, int offset, int sequenceIdx, boolean channelInfo) throws IOException {
- if (channelInfo) {
- if (data[offset++] != (LEDGER_DEFAULT_CHANNEL >> 8)) {
- throw new IOException("Invalid channel");
- }
-
- if (data[offset++] != (LEDGER_DEFAULT_CHANNEL & 0xff)) {
- throw new IOException("Invalid channel");
- }
- }
-
- if (data[offset++] != TAG_APDU) {
- throw new IOException("Invalid tag");
- }
-
- if (data[offset++] != (sequenceIdx >> 8)) {
- throw new IOException("Invalid sequence");
- }
-
- if (data[offset++] != (sequenceIdx & 0xff)) {
- throw new IOException("Invalid sequence");
- }
- return offset;
- }
-
- private static byte[] wrapCommandAPDU(byte[] command, int segmentSize, boolean channelInfo) {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
-
- int headerSize = channelInfo ? 5 : 3;
- int initialHeaderSize = headerSize + 2;
-
- int sequenceIdx = 0;
- int offset = 0;
- writeCommandHeader(output, sequenceIdx, channelInfo);
- sequenceIdx++;
-
- output.write(command.length >> 8);
- output.write(command.length);
- int blockSize = (command.length > (segmentSize - initialHeaderSize) ? (segmentSize - initialHeaderSize) : command.length);
- output.write(command, offset, blockSize);
- offset += blockSize;
-
- while (offset != command.length) {
- writeCommandHeader(output, sequenceIdx, channelInfo);
- sequenceIdx++;
-
- blockSize = ((command.length - offset) > (segmentSize - headerSize) ? (segmentSize - headerSize) : (command.length - offset));
- output.write(command, offset, blockSize);
- offset += blockSize;
- }
-
- if ((output.size() % segmentSize) != 0) {
- byte[] padding = new byte[segmentSize - (output.size() % segmentSize)];
- output.write(padding, 0, padding.length);
- }
-
- return output.toByteArray();
- }
-
- private static void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx, boolean channelInfo) {
- if (channelInfo) {
- output.write(LEDGER_DEFAULT_CHANNEL >> 8);
- output.write(LEDGER_DEFAULT_CHANNEL);
- }
-
- output.write(TAG_APDU);
- output.write(sequenceIdx >> 8);
- output.write(sequenceIdx);
- }
-}
diff --git a/settings.gradle b/settings.gradle
index bf5b867..0d6f95f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,3 @@
include 'lib'
-include 'android'
-include 'desktop'
-include 'demo-android'
+//include 'android'
+include 'desktop'
\ No newline at end of file