28 Commits
Author SHA1 Message Date
Michele Balistreri 5ea1a2397b applet can now be loaded also on "fresh" JCOP cards 2018-12-29 11:53:13 +03:00
Michele Balistreri 7511d3ea6c version 2.0.0 2018-12-14 14:44:18 +03:00
Michele Balistreri a9982c5fdc add iml file back to ignored files 2018-12-12 10:26:48 +03:00
Michele Balistreri 73192b3ba0 update README 2018-12-12 10:22:28 +03:00
Bitgamma a22528dc0f Merge pull request #13 from status-im/nfc-spot
bump version to 0.0.7
2018-12-12 10:12:36 +03:00
Andrea Franz ffd5b1a83f bump version to 0.0.7 2018-12-11 15:59:30 +01:00
Bitgamma dc2171f1dd Merge pull request #12 from status-im/nfc-spot
add button to show nfc spot
2018-12-11 17:59:03 +03:00
Andrea Franz c91e4d7848 check if NFCSpot is available 2018-12-11 15:48:12 +01:00
Andrea Franz c8f13db627 add button to show nfc spot 2018-12-11 14:57:55 +01:00
Bitgamma 98caa6106e rename to keycard (#11)
* rename to keycard

* clean imports

* update to latest SDK

* use unified SDK
2018-12-11 14:30:01 +01:00
Michele Balistreri 85177fe5d3 update applet and PerfTest 2018-11-30 11:35:01 +03:00
Andrea Franz 0045a2259a Merge pull request #10 from status-im/globalplatform-from-sdk
add missing file Secrets.java
2018-11-27 13:59:28 +01:00
Andrea Franz 2e6a10fe81 update Secrets 2018-11-27 13:52:45 +01:00
Bitgamma e708ef9419 Merge pull request #9 from status-im/globalplatform-from-sdk
Globalplatform from sdk
2018-11-27 15:08:11 +03:00
Andrea Franz 0cd03ca32f remove all globsalplatform code and use the sdk commandset 2018-11-27 12:57:36 +01:00
Andrea Franz 084baa5501 import local hardwallet-lite-android 2018-11-21 14:56:40 +01:00
Bitgamma 3f8a6c94dc Merge pull request #8 from status-im/use-sdk
simplify NDEF configuration
2018-11-13 14:28:29 +03:00
Michele Balistreri 341c5cbe1d simplify NDEF configuration 2018-11-13 14:27:26 +03:00
Bitgamma a13b70d2ac Merge pull request #7 from status-im/use-sdk
use the SDK instead of duplicating classes
2018-11-12 19:45:49 +03:00
Michele Balistreri 60faf16d61 use the SDK instead of duplicating classes 2018-11-12 13:30:46 +03:00
Bitgamma 77d5774d22 Merge pull request #5 from ligi/fix_4
Replace java.util.Base64 with android.util.Base64
2018-10-28 07:26:56 +01:00
ligi d27f55fb59 Replace java.util.Base64 with android.util.Base64
closes #4

context: https://stackoverflow.com/questions/48697766/android-studio-noclassdeffounderror-with-java-util-base64
2018-10-28 05:08:55 +01:00
Bitgamma 8d442959d3 Merge pull request #3 from status-im/ndef-install
Install NDEF applet
2018-10-17 17:36:50 +02:00
Michele Balistreri d3eb43b8e5 revert jdk change 2018-10-16 12:11:32 +02:00
Michele Balistreri e2afb066c3 bump version name to match next GitHub release 2018-10-16 12:09:12 +02:00
Michele Balistreri 0272fadb85 add installation/deletion of NDEF applet 2018-10-16 12:06:55 +02:00
Michele Balistreri cb8c5c8e2b add installation for testing 2018-10-11 12:01:13 +02:00
Bitgamma 00057359ce Merge pull request #2 from status-im/no-inst-params
support secret personalization separate from INSTALL command
2018-10-10 19:49:41 +03:00
60 changed files with 419 additions and 3127 deletions
+2 -11
View File
@@ -1,6 +1,7 @@
/local.properties
/.idea/modules.xml
/.idea
/build
*.iml
# Built application files
*.apk
@@ -36,16 +37,6 @@ proguard/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
.idea/caches
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
-29
View File
@@ -1,29 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>
-43
View File
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+7 -7
View File
@@ -1,11 +1,11 @@
# smartcard-cap-installer-test
# Keycard Installer for Android
This is an android application that implements the basic APDU commands and secure channel to be able to install our [hardware wallet cap](https://github.com/status-im/hardware-wallet)
to a smartcard via NFC.
This is an Android application to install the [keycard applet](https://github.com/status-im/status-keycard) to a
smartcard via NFC. The compiled cap file of the applet is bundled in the application. Currently, version 2.0 of the
applet is installed.
The Purpose of it is to be able to start testing smartcards and NFC connection with different cards and readers.
This can help with UX/UI design while we implement the final solution in `status-react` and `status-go`.
If you work with the Status Keycard this is a quick utility to reinstall the applet.
[Download APK](https://github.com/status-im/smartcard-cap-installer-test/releases/download/0.0.1/app-debug.apk)
[Download APK](https://github.com/status-im/keycard-installer-android/releases/download/2.0.0/keycard-installer-debug.apk)
![https://github.com/status-im/smartcard-cap-installer-test/blob/master/docs/demo.gif?raw=true](https://github.com/status-im/smartcard-cap-installer-test/blob/master/docs/demo.gif?raw=true)
![https://github.com/status-im/keycard-installer-android/blob/master/docs/demo.gif?raw=true](https://github.com/status-im/keycard-installer-android/blob/master/docs/demo.gif?raw=true)
+6 -5
View File
@@ -3,11 +3,11 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "im.status.applet_installer_test.appletinstaller"
applicationId "im.status.keycard_installer.installer"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "0.0.2"
versionCode 201
versionName "2.0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -22,8 +22,9 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation 'com.github.status-im.status-keycard-java:android:2.0.0'
implementation 'de.cotech:nfc-sweetspot:1.1'
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'
@@ -1,26 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("im.status.applet_installer_test.appletinstaller", appContext.getPackageName());
}
}
+7 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.status.applet_installer_test.appletinstaller">
package="im.status.keycard.installer">
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce"
@@ -13,13 +13,18 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<activity android:name="im.status.keycard.installer.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="de.cotech.sweetspot.ShowNfcSweetspotActivity"
android:exported="false"
/>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
@@ -1,67 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
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;
this.ins = ins;
this.p1 = p1;
this.p2 = p2;
this.data = data;
this.needsLE = needsLE;
}
public byte[] serialize() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(this.cla);
out.write(this.ins);
out.write(this.p1);
out.write(this.p2);
out.write(this.data.length);
out.write(this.data);
if (this.needsLE) {
out.write(0); // Response length
}
return out.toByteArray();
}
public int getCla() {
return cla;
}
public int getIns() {
return ins;
}
public int getP1() {
return p1;
}
public int getP2() {
return p2;
}
public byte[] getData() {
return data;
}
public boolean getNeedsLE() {
return this.needsLE;
}
}
@@ -1,15 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
public class APDUException extends Exception {
public final int sw;
public APDUException(int sw, String message) {
super(message + ", 0x" + String.format("%04X", sw));
this.sw = sw;
}
public APDUException(String message) {
super(message);
this.sw = 0;
}
}
@@ -1,69 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.IOException;
public class APDUResponse {
public static int SW_OK = 0x9000;
public static int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
public static int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
public static int SW_CARD_LOCKED = 0x6283;
public static int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static 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 {
if (!isOK()) {
throw new APDUException(this.getSw(), "Unexpected error SW");
}
return this;
}
public byte[] getData() {
return this.data;
}
public int getSw() {
return this.sw;
}
public int getSw1() {
return this.sw1;
}
public int getSw2() {
return this.sw2;
}
public byte[] getBytes() {
return this.apdu;
}
}
@@ -1,60 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
public class APDUWrapper {
private byte[] macKeyData;
private byte[] icv;
public APDUWrapper(byte[] macKeyData) {
this.macKeyData = macKeyData;
this.icv = Crypto.NullBytes8.clone();
}
public APDUCommand wrap(APDUCommand cmd) {
try {
int cla = (cmd.getCla() | 0x04) & 0xff;
byte[] data = cmd.getData();
ByteArrayOutputStream macData = new ByteArrayOutputStream();
macData.write(cla);
macData.write(cmd.getIns());
macData.write(cmd.getP1());
macData.write(cmd.getP2());
macData.write(data.length + 8);
macData.write(data);
byte[] icv;
if (Arrays.equals(this.icv, Crypto.NullBytes8)) {
icv = this.icv;
} else {
icv = Crypto.encryptICV(this.macKeyData, this.icv);
}
byte[] mac = Crypto.macFull3des(this.macKeyData, Crypto.appendDESPadding(macData.toByteArray()), icv);
byte[] newData = new byte[data.length + mac.length];
System.arraycopy(data, 0, newData, 0, data.length );
System.arraycopy(mac, 0, newData, data.length, mac.length );
APDUCommand wrapped = new APDUCommand(cla, cmd.getIns(), cmd.getP1(), cmd.getP2(), newData, cmd.getNeedsLE());
this.icv = mac.clone();
return wrapped;
} catch (IOException e) {
throw new RuntimeException("error wrapping APDU command.", e);
}
}
public byte[] getICV() {
return this.icv;
}
}
@@ -1,23 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.nfc.tech.IsoDep;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.CardManager;
public class CardChannel implements Channel {
private IsoDep isoDep;
public CardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Logger.d(String.format("COMMAND %s", HexUtils.byteArrayToHexString(apdu)));
byte[] resp = this.isoDep.transceive(apdu);
Logger.d(String.format("RESPONSE %s %n-----------------------", HexUtils.byteArrayToHexString(resp)));
return new APDUResponse(resp);
}
}
@@ -1,141 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.content.res.AssetManager;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import java.io.IOException;
public class CardManager extends Thread implements NfcAdapter.ReaderCallback {
public final static int ACTION_NONE = 0;
public final static int ACTION_INSTALL = 1;
public final static int ACTION_PERFTEST = 2;
private NfcAdapter nfcAdapter;
private AssetManager assets;
private String capPath;
private IsoDep isoDep;
private int requestedAction;
private long cardConnectedAt;
private boolean running;
public CardManager(NfcAdapter nfcAdapter, AssetManager assets, String capPath) {
this.nfcAdapter = nfcAdapter;
this.assets = assets;
this.capPath = capPath;
this.requestedAction = ACTION_NONE;
}
public boolean isConnected() {
return this.isoDep != null && this.isoDep.isConnected();
}
public void requestAction(int actionRequested) {
switch(actionRequested) {
case ACTION_NONE:
Logger.i("cancelling requested action");
break;
case ACTION_INSTALL:
Logger.i("installation requested");
break;
case ACTION_PERFTEST:
Logger.i("performance tests requested");
break;
default:
Logger.i("invalid action requested, ignoring");
return;
}
this.requestedAction = actionRequested;
}
@Override
public void onTagDiscovered(Tag tag) {
this.isoDep = IsoDep.get(tag);
try {
this.isoDep = IsoDep.get(tag);
this.isoDep.connect();
this.isoDep.setTimeout(120000);
} catch (IOException e) {
Logger.e("error connecting to tag");
}
}
public void run() {
boolean connected = this.isConnected();
while(true) {
boolean newConnected = this.isConnected();
if (newConnected != connected) {
connected = newConnected;
Logger.i("tag " + (connected ? "connected" : "disconnected"));
if (connected) {
this.onCardConnected();
} else {
this.onCardDisconnected();
}
}
if (connected && (this.requestedAction != ACTION_NONE) && !this.running) {
long now = System.currentTimeMillis();
if (now - this.cardConnectedAt > 2000) {
this.perform();
}
}
try {
this.sleep(50);
} catch (InterruptedException e) {
Logger.e("error in TagManager thread: " + e.getMessage());
this.interrupt();
}
}
}
private void onCardConnected() {
this.cardConnectedAt = System.currentTimeMillis();
if (this.requestedAction != ACTION_NONE) {
Logger.i("waiting 2 seconds to start requested action");
} else {
Logger.i("no action requested yet");
}
}
private void onCardDisconnected() {
this.cardConnectedAt = 0;
this.isoDep = null;
}
private void perform() {
Logger.i("starting requested action");
this.running = true;
try {
CardChannel ch = new CardChannel(this.isoDep);
switch (requestedAction) {
case ACTION_INSTALL:
Installer installer = new Installer(ch, this.assets, this.capPath);
installer.start();
break;
case ACTION_PERFTEST:
PerfTest perfTest = new PerfTest(ch);
perfTest.test();
break;
default:
throw new Exception("Unknown action");
}
} catch (IOException e) {
Logger.e("IO exception: " + e.getMessage());
} catch (APDUException e) {
Logger.e("APDU exception: " + e.getMessage());
} catch (Exception e) {
Logger.e("Other exception: " + e.getMessage());
} finally {
this.running = false;
this.requestedAction = ACTION_NONE;
this.cardConnectedAt = 0;
}
}
}
@@ -1,7 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.IOException;
public interface Channel {
APDUResponse send(APDUCommand cmd) throws IOException;
}
@@ -1,166 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
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 java.util.Base64;
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;
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[] deriveKey(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 length = data.length + 1;
for (; length % 8 != 0; length++){
}
byte[] newData = new byte[length];
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[] generatePairingKey(char[] pairing) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
String salt = "Status Hardware Wallet Lite";
PBEKeySpec spec = new PBEKeySpec(pairing, salt.getBytes(), 50000, 32*8);
SecretKey key = skf.generateSecret(spec);
return key.getEncoded();
}
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) {
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
String token = encoder.encodeToString(randomBytes(length));
return token;
}
}
@@ -1,30 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
public class HexUtils {
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len/2];
for(int i = 0; i < len; i+=2){
data[i/2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
}
return data;
}
final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
public static String byteArrayToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
int v;
for(int j=0; j < bytes.length; j++) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v>>>4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}
@@ -1,140 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.content.res.AssetManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import im.status.applet_installer_test.appletinstaller.apducommands.*;
public class Installer {
private Channel plainChannel;
private Channel channel;
private Keys cardKeys;
private AssetManager assets;
private String capPath;
static final byte[] cardKeyData = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
public Installer(Channel channel, AssetManager assets, String capPath) {
this.plainChannel = channel;
this.channel = channel;
this.cardKeys = new Keys(cardKeyData, cardKeyData);
this.assets = assets;
this.capPath = capPath;
}
public void start() throws IOException, APDUException, NoSuchAlgorithmException, InvalidKeySpecException {
Logger.i("installation started");
long startTime = System.currentTimeMillis();
Select discover = new Select(new byte[0]);
APDUResponse resp = this.send("discover", discover.getCommand());
byte[] sdaid = this.getSDAID(resp.getData());
Logger.d("sdaid: " + HexUtils.byteArrayToHexString(sdaid));
byte[] hostChallenge = InitializeUpdate.generateChallenge();
InitializeUpdate init = new InitializeUpdate(hostChallenge);
resp = this.send("init update", init.getCommand());
Session session = init.verifyResponse(this.cardKeys, resp);
Keys sessionKeys = session.getKeys();
this.channel = new SecureChannel(this.channel, sessionKeys);
ExternalAuthenticate auth = new ExternalAuthenticate(sessionKeys.getEncKeyData(), session.getCardChallenge(), hostChallenge);
resp = this.send("external auth", auth.getCommand());
if (!auth.checkResponse(resp)) {
throw new APDUException(resp.getSw(), "bad external authenticate response");
}
//Status status = new Status(Status.P1_EXECUTABLE_LOAD_FILES_AND_MODULES);
//resp = this.send("status", status.getCommand());
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
Delete deleteApplet = new Delete(appletAID);
Logger.i("sending delete (applet)");
this.channel.send(deleteApplet.getCommand());
Delete deletePkg = new Delete(aid);
Logger.i("sending delete (pkg)");
this.channel.send(deletePkg.getCommand());
InstallForLoad preLoad = new InstallForLoad(aid, sdaid);
this.send("perform for load", preLoad.getCommand());
//URL url = this.getClass().getClassLoader().getResource("wallet.cap");
InputStream in = this.assets.open(this.capPath);
Load load = new Load(in);
APDUCommand loadCmd;
while((loadCmd = load.getCommand()) != null) {
this.send("load " + load.getCount() + "/37", loadCmd);
}
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, new byte[0]);
this.send("perform and make selectable", install.getCommand());
installSecrets();
long duration = System.currentTimeMillis() - startTime;
Logger.i(String.format("installation completed in %d seconds", duration / 1000));
}
private void installSecrets() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException {
Secrets secrets = Secrets.generate();
Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s\nPairing token: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword(), HexUtils.byteArrayToHexString(secrets.getPairingToken())));
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet((CardChannel) this.plainChannel);
byte[] ecKey = cmdSet.select().checkOK().getData();
SecureChannelSession secureChannel = new SecureChannelSession(Arrays.copyOfRange(ecKey, 2, ecKey.length));
cmdSet.setSecureChannel(secureChannel);
cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingToken()).checkOK();
}
private APDUResponse send(String description, APDUCommand cmd) throws IOException, APDUException {
Logger.d("sending command " + description);
APDUResponse resp = this.channel.send(cmd);
if(resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
Logger.e("SW_SECURITY_CONDITION_NOT_SATISFIED: card might be blocked");
throw new APDUException(resp.getSw(), "security confition not satisfied. card might be blocked " + description);
}
if (!resp.isOK()) {
throw new APDUException(resp.getSw(), "bad response for command " + description);
}
return resp;
}
private byte[] getSDAID(byte[] data) throws IOException, APDUException {
Tlv tlv = new Tlv(data);
tlv = tlv.find((byte) 0x6F);
if (tlv == null) {
throw new APDUException("error searching for tag 0x6F in discover response");
}
tlv = tlv.find((byte) 0x84);
if (tlv == null) {
throw new APDUException("error searching for tag 0x84 in discover response");
}
return tlv.getValue();
}
}
@@ -1,19 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
public class Keys {
public byte[] encKeyData;
public byte[] macKeyData;
public Keys(byte[] encKeyData, byte[] macKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
}
public byte[] getEncKeyData() {
return encKeyData;
}
public byte[] getMacKeyData() {
return macKeyData;
}
}
@@ -1,235 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.util.Log;
import im.status.applet_installer_test.appletinstaller.apducommands.SecureChannelSession;
import im.status.applet_installer_test.appletinstaller.apducommands.WalletAppletCommandSet;
import org.spongycastle.asn1.ASN1InputStream;
import org.spongycastle.asn1.ASN1Integer;
import org.spongycastle.asn1.DLSequence;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.crypto.ec.CustomNamedCurves;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.crypto.signers.ECDSASigner;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.math.ec.FixedPointUtil;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Random;
public class PerfTest {
private CardChannel cardChannel;
private WalletAppletCommandSet cmdSet;
private SecureChannelSession secureChannel;
private long openSecureChannelTime = 0;
private long loadKeysTime = 0;
private long loginTime = 0;
private long signTime = 0;
private long deriveKeyFromParent = 0;
private long getStatusTime = 0;
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
public static final ECDomainParameters CURVE;
static {
FixedPointUtil.precompute(CURVE_PARAMS.getG(), 12);
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
byte[] tmp;
try {
tmp = Crypto.generatePairingKey(new char[] {'W', 'a', 'l', 'l', 'e', 't', 'A','p', 'p', 'l', 'e', 't', 'T', 'e', 's', 't'});
} catch (Exception e) {
tmp = null;
}
SHARED_SECRET = tmp;
}
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
static final byte EXPORT_KEY_P1_HIGH = 0x01;
static final byte SIGN_P1_PRECOMPUTED_HASH = 0x01;
static final byte GET_STATUS_P1_APPLICATION = 0x00;
static final byte GET_STATUS_P1_KEY_PATH = 0x01;
// m/44'/60'/0'/0/0
static final byte[] BIP44_PATH = new byte[] { (byte) 0x80, 0x00, 0x00, 0x2c, (byte) 0x80, 0x00, 0x00, 0x3c, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00};
// TODO: Make this an input
public static final byte[] SHARED_SECRET;
public PerfTest(CardChannel cardChannel) {
this.cardChannel = cardChannel;
}
public void test() throws Exception {
cmdSet = new WalletAppletCommandSet(cardChannel);
byte[] keyData = extractPublicKeyFromSelect(cmdSet.select().getData());
secureChannel = new SecureChannelSession(keyData);
cmdSet.setSecureChannel(secureChannel);
cmdSet.autoPair(SHARED_SECRET);
openSecureChannelTime = System.currentTimeMillis();
cmdSet.autoOpenSecureChannel();
openSecureChannelTime = System.currentTimeMillis() - openSecureChannelTime;
cmdSet.verifyPIN("000000").checkOK();
cmdSet.unpairOthers(); // Recover in case of non-clean termination
Logger.i("Measuring performances. Logging disabled. Please wait");
Logger.setLevel(Log.INFO);
try {
loadKeys();
getStatus();
login();
signTransactions();
} finally {
Logger.setLevel(Log.INFO);
Logger.setUILevel(Log.INFO);
}
Logger.i("Reenabling logging.");
cmdSet.select();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
cmdSet.autoUnpair();
Logger.i("*************************************************");
Logger.i("Opening Secure Channel: " + openSecureChannelTime + "ms");
Logger.i("Derivation of m/44'/60'/0'/0/0 from master: " + loadKeysTime + "ms");
Logger.i("All following measurements are from application selection to the last needed APDU");
Logger.i("GET STATUS: " + getStatusTime + "ms");
Logger.i("Login: " + loginTime + "ms");
Logger.i("Transaction signature (after login): " + signTime + "ms");
Logger.i("Transaction signature (subsequent): " + (signTime - deriveKeyFromParent) + "ms");
}
private void getStatus() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select();
cmdSet.autoOpenSecureChannel();
cmdSet.getStatus(GET_STATUS_P1_APPLICATION).checkOK();
getStatusTime = System.currentTimeMillis() - time;
}
private void login() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x00}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK();
cmdSet.exportKey(EXPORT_KEY_P1_HIGH, false).checkOK();
cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x01}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK();
cmdSet.exportKey(EXPORT_KEY_P1_HIGH, false).checkOK();
loginTime = System.currentTimeMillis() - time;
}
private void loadKeys() throws Exception {
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = g.generateKeyPair();
byte[] chainCode = new byte[32];
new Random().nextBytes(chainCode);
cmdSet.loadKey(keyPair, false, chainCode).checkOK();
long time = System.currentTimeMillis();
cmdSet.deriveKey(BIP44_PATH, DERIVE_P1_SOURCE_CURRENT, false, false).checkOK();
loadKeysTime = System.currentTimeMillis() - time;
}
private void signTransactions() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
deriveKeyFromParent = System.currentTimeMillis();
cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, DERIVE_P1_SOURCE_PARENT, false, false).checkOK();
deriveKeyFromParent = System.currentTimeMillis() - deriveKeyFromParent;
cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes(), SIGN_P1_PRECOMPUTED_HASH, true, true).checkOK();
signTime = System.currentTimeMillis() - time;
}
private KeyPairGenerator keypairGenerator() throws Exception {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
g.initialize(ecSpec);
return g;
}
private byte[] extractPublicKeyFromSelect(byte[] select) {
return Arrays.copyOfRange(select, 22, 22 + select[21]);
}
private byte[] derivePublicKey(byte[] data) throws Exception {
byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]);
byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length);
byte[] hash = MessageDigest.getInstance("SHA256").digest("STATUS KEY DERIVATION".getBytes());
pubKey[0] = 0x02;
ECPoint candidate = CURVE.getCurve().decodePoint(pubKey);
if (!verifySig(hash, signature, candidate)) {
pubKey[0] = 0x03;
candidate = CURVE.getCurve().decodePoint(pubKey);
if (!verifySig(hash, signature, candidate)) {
throw new Exception("Public key is incorrect");
}
}
return candidate.getEncoded(false);
}
private boolean verifySig(byte[] hash, byte[] signature, ECPoint pub) {
ECDSASigner signer = new ECDSASigner();
ECPublicKeyParameters params = new ECPublicKeyParameters(pub, CURVE);
signer.init(false, params);
ECDSASignature sig = ECDSASignature.decodeFromDER(signature);
return signer.verifySignature(hash, sig.r, sig.s);
}
static class ECDSASignature {
/** The two components of the signature. */
public final BigInteger r, s;
/**
* Constructs a signature with the given components. Does NOT automatically canonicalise the signature.
*/
public ECDSASignature(BigInteger r, BigInteger s) {
this.r = r;
this.s = s;
}
public static ECDSASignature decodeFromDER(byte[] bytes) {
ASN1InputStream decoder = null;
try {
decoder = new ASN1InputStream(bytes);
DLSequence seq = (DLSequence) decoder.readObject();
if (seq == null)
throw new RuntimeException("Reached past end of ASN.1 stream.");
ASN1Integer r, s;
try {
r = (ASN1Integer) seq.getObjectAt(0);
s = (ASN1Integer) seq.getObjectAt(1);
} catch (ClassCastException e) {
throw new IllegalArgumentException(e);
}
// OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
// Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (decoder != null)
try { decoder.close(); } catch (IOException x) {}
}
}
}
}
@@ -1,48 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.support.annotation.NonNull;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
public class Secrets {
private String pin;
private String puk;
private String pairingPassword;
private byte[] pairingToken;
public Secrets(String pin, String puk, String pairingPassword, byte[] pairingToken) {
this.pin = pin;
this.puk = puk;
this.pairingPassword = pairingPassword;
this.pairingToken = pairingToken;
}
@NonNull
public static Secrets generate() throws NoSuchAlgorithmException, InvalidKeySpecException {
String pairingPassword = Crypto.randomToken(12);
byte[] pairingToken = Crypto.generatePairingKey(pairingPassword.toCharArray());
long pinNumber = Crypto.randomLong(Crypto.PIN_BOUND);
long pukNumber = Crypto.randomLong(Crypto.PUK_BOUND);
String pin = String.format("%06d", pinNumber);
String puk = String.format("%012d", pukNumber);
return new Secrets(pin, puk, pairingPassword, pairingToken);
}
public String getPin() {
return pin;
}
public String getPuk() {
return puk;
}
public String getPairingPassword() {
return pairingPassword;
}
public byte[] getPairingToken() {
return pairingToken;
}
}
@@ -1,22 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.provider.Settings;
import java.io.IOException;
public class SecureChannel implements Channel {
private Channel channel;
private APDUWrapper wrapper;
public SecureChannel(Channel channel, Keys keys) {
this.channel = channel;
this.wrapper = new APDUWrapper(keys.getMacKeyData());
}
public APDUResponse send(APDUCommand cmd) throws IOException {
Logger.d(String.format("WRAPPING %s %n", HexUtils.byteArrayToHexString(cmd.serialize())), false);
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
Logger.d(String.format("WRAPPED %s %n", HexUtils.byteArrayToHexString(wrappedCommand.serialize())), false);
return this.channel.send(wrappedCommand);
}
}
@@ -1,19 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
public class Session {
private Keys keys;
private byte[] cardChallenge;
public Session(Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
}
public Keys getKeys() {
return keys;
}
public byte[] getCardChallenge() {
return cardChallenge;
}
}
@@ -1,52 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.IOException;
public class Tlv {
private byte tag;
private byte[] value;
public Tlv(byte[] value) {
this((byte) 0x00, value);
}
public Tlv(byte tag, byte[] value) {
this.tag = tag;
this.value = value;
}
public Tlv find(byte tag) throws IOException {
int offset = 0;
while(offset + 2 < value.length) {
byte childTag = this.value[offset];
offset++;
int childLength = this.value[offset] & 0xff;
offset++;
if (offset + childLength - 1 >= this.value.length) {
return null;
}
byte[] data = new byte[childLength];
System.arraycopy(this.value, offset, data, 0, childLength);
offset += childLength;
if (childTag == tag) {
return new Tlv(childTag, data);
}
}
return null;
}
public byte[] getValue() {
return value;
}
public byte getTag() {
return tag;
}
}
@@ -1,26 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Delete {
private static final int CLA = 0x80;
private static final int INS = 0xE4;
private static final int P1 = 0x00;
//private static final int P2 = 0x80; // delete object and related files
private static final int P2 = 0x00;
private byte[] aid;
public Delete(byte[] aid) {
this.aid = aid;
}
public APDUCommand getCommand() {
byte[] data = new byte[this.aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) this.aid.length;
System.arraycopy(this.aid, 0, data, 2, this.aid.length);
return new APDUCommand(CLA, INS, P1, P2, data);
}
}
@@ -1,39 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.Crypto;
public class ExternalAuthenticate {
public static int CLA = 0x84;
public static int INS = 0x82;
public static int P1 = 0x01;
public static int P2 = 0x00;
private byte[] encKeyData;
private byte[] cardChallenge;
private byte[] hostChallenge;
public ExternalAuthenticate(byte[] encKeyData, byte[] cardChallenge, byte[] hostChallenge) {
this.encKeyData = encKeyData;
this.cardChallenge = cardChallenge;
this.hostChallenge = hostChallenge;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.getHostCryptogram());
}
public byte[] getHostCryptogram() {
byte[] data = new byte[this.cardChallenge.length + this.hostChallenge.length];
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
byte[] paddedData = Crypto.appendDESPadding(data);
return Crypto.mac3des(this.encKeyData, paddedData, Crypto.NullBytes8);
}
public boolean checkResponse(APDUResponse resp) {
return resp.getSw() == APDUResponse.SW_OK;
}
}
@@ -1,8 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
public class GetData {
public static final int CLA = 0x80;
public static final int INS = 0x50;
public static final int P1 = 0;
public static final int P2 = 0;
}
@@ -1,74 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.security.SecureRandom;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.Crypto;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import im.status.applet_installer_test.appletinstaller.Keys;
import im.status.applet_installer_test.appletinstaller.Logger;
import im.status.applet_installer_test.appletinstaller.Session;
public class InitializeUpdate {
public static final int CLA = 0x80;
public static final int INS = 0x50;
public static final int P1 = 0;
public static final int P2 = 0;
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};
private byte[] hostChallenge;
public InitializeUpdate(byte[] challenge) {
this.hostChallenge = challenge;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.hostChallenge, true);
}
public static byte[] generateChallenge() {
return Crypto.randomBytes(8);
}
public Session verifyResponse(Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
throw new APDUException(resp.getSw(), "security confition 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.deriveKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
Keys sessionKeys = new Keys(sessionEncKey, sessionMacKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), this.hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
throw new APDUException("error verifying card cryptogram.");
}
return new Session(sessionKeys, cardChallenge);
}
}
@@ -1,52 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class InstallForInstall {
public static final int CLA = 0x80;
public static final int INS = 0xE6;
public static final int P1 = 0x0C;
public static final int P2 = 0;
private byte[] packageAID;
private byte[] appletAID;
private byte[] instanceAID;
private byte[] params;
public InstallForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) {
this.packageAID = packageAID;
this.appletAID = appletAID;
this.instanceAID = instanceAID;
this.params = params;
}
public APDUCommand getCommand() throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(this.packageAID.length);
data.write(this.packageAID);
data.write(this.appletAID.length);
data.write(this.appletAID);
data.write(this.instanceAID.length);
data.write(this.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);
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray() );
}
}
@@ -1,36 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class InstallForLoad {
public static final int CLA = 0x80;
public static final int INS = 0xE6;
public static final int P1 = 0x02;
public static final int P2 = 0;
private byte[] aid;
private byte[] sdaid;
public InstallForLoad(byte[] aid, byte[] sdaid) {
this.aid = aid;
this.sdaid = sdaid;
}
public APDUCommand getCommand() throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(this.aid.length);
data.write(this.aid);
data.write(this.sdaid.length);
data.write(this.sdaid);
// empty hash length and hash
data.write(0x00);
data.write(0x00);
data.write(0x00);
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray());
}
}
@@ -1,135 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
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.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
public class Load {
public static final int CLA = 0x80;
public static final int INS = 0xE8;
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
private String path;
private int offset;
private int count;
private byte[] fullData;
public Load(InputStream in) throws FileNotFoundException, IOException {
this.path = path;
this.offset = 0;
this.count = 0;
Map<String, byte[]> files = this.loadFiles(in);
in.close();
this.fullData = this.getCode(files);
}
public Map<String, byte[]> loadFiles(InputStream in) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
ZipInputStream zip = new ZipInputStream(in);
ZipEntry entry = zip.getNextEntry();
while(entry != null) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count;
while ((count = zip.read(buf)) != -1) {
data.write(buf, 0, count);
}
String name = baseName(entry.getName());
files.put(name, data.toByteArray());
entry = zip.getNextEntry();
}
return files;
}
private String baseName(String path) {
String[] parts = path.split("[/.]");
return parts[parts.length - 2];
}
public APDUCommand getCommand() {
int blockSize = 247; // 255 - 8 bytes for MAC
if (this.offset >= this.fullData.length) {
return null;
}
int rangeEnd = this.offset + blockSize;
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);
boolean isLast = this.offset + size >= this.fullData.length;
int p1 = isLast ? 0x80 : 0;
APDUCommand cmd = new APDUCommand(CLA, INS, p1, this.count, data);
this.offset += size;
this.count++;
return cmd;
}
private byte[] encodeFullLength(int length) {
if (length < 0x80) {
return new byte[]{(byte) length};
} else if (length < 0xFF) {
return new byte[]{(byte) 0x81, (byte) length};
} else if (length < 0xFFFF) {
return new byte[]{
(byte) 0x82,
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
} else {
return new byte[]{
(byte) 0x83,
(byte) ((length & 0xFF0000) >> 16),
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
}
}
public byte[] getCode(Map<String, byte[]> files) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
for (String name : fileNames) {
byte[] fileData = files.get(name);
if (fileData == null) {
continue;
}
dataStream.write(fileData);
}
byte[] data = dataStream.toByteArray();
byte[] encodedFullLength = encodeFullLength(data.length);
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
fullData[0] = (byte) 0xC4;
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
return fullData;
}
public int getCount() {
return count;
}
}
@@ -1,474 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.*;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.macs.CBCBlockCipherMac;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.*;
import java.util.Arrays;
/**
* Handles a SecureChannel session with the card.
*/
public class SecureChannelSession {
public static final short SC_SECRET_LENGTH = 32;
public static final short SC_BLOCK_SIZE = 16;
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11;
public static final byte INS_PAIR = 0x12;
public static final byte INS_UNPAIR = 0x13;
public static final byte PAIR_P1_FIRST_STEP = 0x00;
public static final byte PAIR_P1_LAST_STEP = 0x01;
public static final int PAYLOAD_MAX_SIZE = 223;
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
private byte[] secret;
private byte[] publicKey;
private byte[] pairingKey;
private byte[] iv;
private byte pairingIndex;
private Cipher sessionCipher;
private CBCBlockCipherMac sessionMac;
private SecretKeySpec sessionEncKey;
private KeyParameter sessionMacKey;
private SecureRandom random;
private boolean open;
/**
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
* The public key of the card is used as input for the EC-DH algorithm. The output is stored as the secret.
*
* @param keyData the public key returned by the applet as response to the SELECT command
*/
public SecureChannelSession(byte[] keyData) {
random = new SecureRandom();
generateSecret(keyData);
open = false;
}
public void generateSecret(byte[] keyData) {
try {
random = new SecureRandom();
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
g.initialize(ecSpec, random);
KeyPair keyPair = g.generateKeyPair();
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
keyAgreement.init(keyPair.getPrivate());
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec);
keyAgreement.doPhase(cardKey, true);
secret = keyAgreement.generateSecret();
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Returns the public key
* @return the public key
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* Returns the pairing index
* @return the pairing index
*/
public byte getPairingIndex() {
return pairingIndex;
}
/**
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
* Follows the specifications from the SECURE_CHANNEL.md document.
*
* @param apduChannel the apdu channel
* @return the card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
APDUResponse response = openSecureChannel(apduChannel, pairingIndex, publicKey);
if (response.getSw() != 0x9000) {
throw new IOException("OPEN SECURE CHANNEL failed");
}
processOpenSecureChannelResponse(response);
response = mutuallyAuthenticate(apduChannel);
if (response.getSw() != 0x9000) {
throw new IOException("MUTUALLY AUTHENTICATE failed");
}
if(!verifyMutuallyAuthenticateResponse(response)) {
throw new IOException("Invalid authentication data from the card");
}
}
/**
* Processes the response from OPEN SECURE CHANNEL. This initialize the session keys, Cipher and MAC internally.
*
* @param response the card response
*/
public void processOpenSecureChannelResponse(APDUResponse response) {
try {
MessageDigest md = MessageDigest.getInstance("SHA512");
md.update(secret);
md.update(pairingKey);
byte[] data = response.getData();
byte[] keyData = md.digest(Arrays.copyOf(data, SC_SECRET_LENGTH));
iv = Arrays.copyOfRange(data, SC_SECRET_LENGTH, data.length);
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SC_SECRET_LENGTH), "AES");
sessionMacKey = new KeyParameter(keyData, SC_SECRET_LENGTH, SC_SECRET_LENGTH);
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
open = true;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Verify that the response from MUTUALLY AUTHENTICATE is correct.
*
* @param response the card response
* @return true if response is correct, false otherwise
*/
public boolean verifyMutuallyAuthenticateResponse(APDUResponse response) {
return response.getData().length == SC_SECRET_LENGTH;
}
/**
* Handles the entire pairing procedure in order to be able to use the secure channel
*
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException {
byte[] challenge = new byte[32];
random.nextBytes(challenge);
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 1");
}
byte[] respData = resp.getData();
byte[] cardCryptogram = Arrays.copyOf(respData, 32);
byte[] cardChallenge = Arrays.copyOfRange(respData, 32, respData.length);
byte[] checkCryptogram;
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
md.update(sharedSecret);
checkCryptogram = md.digest(challenge);
if (!Arrays.equals(checkCryptogram, cardCryptogram)) {
throw new IOException("Invalid card cryptogram");
}
md.update(sharedSecret);
checkCryptogram = md.digest(cardChallenge);
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 2");
}
respData = resp.getData();
md.update(sharedSecret);
pairingKey = md.digest(Arrays.copyOfRange(respData, 1, respData.length));
pairingIndex = respData[0];
}
/**
* Unpairs the current paired key
*
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoUnpair(CardChannel apduChannel) throws IOException {
APDUResponse resp = unpair(apduChannel, pairingIndex);
if (resp.getSw() != 0x9000) {
throw new IOException("Unpairing failed");
}
}
/**
* Sends a OPEN SECURE CHANNEL APDU.
*
* @param apduChannel the apdu channel
* @param index the P1 parameter
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws IOException {
open = false;
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, index, 0, data);
return apduChannel.send(openSecureChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException {
byte[] data = new byte[SC_SECRET_LENGTH];
random.nextBytes(data);
return mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU.
*
* @param apduChannel the apdu channel
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws IOException {
APDUCommand mutuallyAuthenticate = protectedCommand(0x80, INS_MUTUALLY_AUTHENTICATE, 0, 0, data);
return transmit(apduChannel, mutuallyAuthenticate);
}
/**
* Sends a PAIR APDU.
*
* @param apduChannel the apdu channel
* @param p1 the P1 parameter
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse pair(CardChannel apduChannel, byte p1, byte[] data) throws IOException {
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
return transmit(apduChannel, openSecureChannel);
}
/**
* Sends a UNPAIR APDU.
*
* @param apduChannel the apdu channel
* @param p1 the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
return transmit(apduChannel, openSecureChannel);
}
/**
* Unpair all other clients
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
for (int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) {
if (i != pairingIndex) {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
transmit(apduChannel, openSecureChannel).checkOK();
}
}
}
/**
* Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext
* already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array
* the returned data will still contain the IV and padding.
*
* @param data the plaintext data
* @return the encrypted data
*/
private byte[] encryptAPDU(byte[] data) {
assert data.length <= PAYLOAD_MAX_SIZE;
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
return sessionCipher.doFinal(data);
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding
* and can be potentially empty.
*
* @param data the ciphetext
* @return the plaintext
*/
private byte[] decryptAPDU(byte[] data) {
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
return sessionCipher.doFinal(data);
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Returns a command APDU with MAC and encrypted data.
*
* @param cla the CLA byte
* @param ins the INS byte
* @param p1 the P1 byte
* @param p2 the P2 byte
* @param data the data, can be an empty array but not null
* @return the command APDU
*/
public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
byte[] finalData;
if (open) {
data = encryptAPDU(data);
byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
updateIV(meta, data);
finalData = Arrays.copyOf(iv, iv.length + data.length);
System.arraycopy(data, 0, finalData, iv.length, data.length);
} else {
finalData = data;
}
return new APDUCommand(cla, ins, p1, p2, finalData);
}
/**
* Transmits a protected command APDU and unwraps the response data. The MAC is verified, the data decrypted and the
* SW read from the payload.
*
* @param apduChannel the APDU channel
* @param apdu the APDU to send
* @return the unwrapped response APDU
* @throws IOException transmission error
*/
public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException {
APDUResponse resp = apduChannel.send(apdu);
if (resp.getSw() == 0x6982) {
open = false;
}
if (open) {
byte[] data = resp.getData();
byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
byte[] mac = Arrays.copyOf(data, iv.length);
data = Arrays.copyOfRange(data, iv.length, data.length);
byte[] plainData = decryptAPDU(data);
updateIV(meta, data);
if (!Arrays.equals(iv, mac)) {
throw new IOException("Invalid MAC");
}
return new APDUResponse(plainData);
} else {
return resp;
}
}
/**
* Marks the SecureChannel as closed
*/
public void reset() {
open = false;
}
/**
* Encrypts the payload for the INIT command
* @param initData the payload for the INIT command
*
* @return the encrypted buffer
*/
public byte[] oneShotEncrypt(byte[] initData) {
try {
iv = new byte[SC_BLOCK_SIZE];
random.nextBytes(iv);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionEncKey = new SecretKeySpec(secret, "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
initData = sessionCipher.doFinal(initData);
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
encrypted[0] = (byte) publicKey.length;
System.arraycopy(publicKey, 0, encrypted, 1, publicKey.length);
System.arraycopy(iv, 0, encrypted, (1 + publicKey.length), iv.length);
System.arraycopy(initData, 0, encrypted, (1 + publicKey.length + iv.length), initData.length);
return encrypted;
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Marks the SecureChannel as open. Only to be used when writing tests for the SecureChannel, in normal operation this
* would only make things wrong.
*
*/
void setOpen() {
open = true;
}
/**
* Calculates a CMAC from the metadata and data provided and sets it as the IV for the next message.
*
* @param meta metadata
* @param data data
*/
private void updateIV(byte[] meta, byte[] data) {
try {
sessionMac.init(sessionMacKey);
sessionMac.update(meta, 0, meta.length);
sessionMac.update(data, 0, data.length);
sessionMac.doFinal(iv, 0);
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
}
@@ -1,20 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Select {
private static final int CLA = 0x00;
private static final int INS = 0xA4;
private static final int P1 = 0x04;
private static final int P2 = 0x00; // first occurrence
private byte[] aid;
public Select(byte[] aid) {
this.aid = aid;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.aid);
}
}
@@ -1,26 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Status {
private static final int CLA = 0x80;
private static final int INS = 0xF2;
public static final int P1_ISSUER_SECURITY_DOMAIN = 0x80;
public static final int P1_APPLICATIONS = 0x40;
public static final int P1_EXECUTABLE_LOAD_FILES = 0x20;
public static final int P1_EXECUTABLE_LOAD_FILES_AND_MODULES = 0x10;
private static final int P2 = 0x02;
private int p1;
public Status(int p1) {
this.p1 = p1;
}
public APDUCommand getCommand() {
byte[] data = new byte[]{0x4F, 0x00};
return new APDUCommand(CLA, INS, this.p1, P2, data, true);
}
}
@@ -1,499 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.CardChannel;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
/**
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
* 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 {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
static final byte INS_LOAD_KEY = (byte) 0xD0;
static final byte INS_DERIVE_KEY = (byte) 0xD1;
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
static final byte INS_REMOVE_KEY = (byte) 0xD3;
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 GET_STATUS_P1_APPLICATION = 0x00;
static final byte LOAD_KEY_P1_EC = 0x01;
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
static final byte LOAD_KEY_P1_SEED = 0x03;
static final byte DERIVE_P1_ASSISTED_MASK = 0x01;
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P2_KEY_PATH = 0x00;
static final byte DERIVE_P2_PUBLIC_KEY = 0x01;
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
static final byte TLV_PUB_KEY = (byte) 0x80;
static final byte TLV_PRIV_KEY = (byte) 0x81;
static final byte TLV_CHAIN_CODE = (byte) 0x82;
public static final String APPLET_AID = "53746174757357616C6C6574417070";
public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
public WalletAppletCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
}
public void setSecureChannel(SecureChannelSession secureChannel) {
this.secureChannel = secureChannel;
}
/**
* Selects the applet. The applet is assumed to have been installed with its default AID. The returned data is a
* public key which must be used to initialize the secure channel.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
if (secureChannel != null) {
secureChannel.reset();
}
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES);
return apduChannel.send(selectApplet);
}
/**
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @return the raw card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel() throws IOException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
/**
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoPair(byte[] sharedSecret) throws IOException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
/**
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoUnpair() throws IOException {
secureChannel.autoUnpair(apduChannel);
}
/**
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate() throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse pair(byte p1, byte[] data) throws IOException {
return secureChannel.pair(apduChannel, p1, data);
}
/**
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse unpair(byte p1) throws IOException {
return secureChannel.unpair(apduChannel, p1);
}
/**
* Unpair all other clients.
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
}
/**
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
* class itself.
*
* @param info the P1 of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse getStatus(byte info) throws IOException {
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
return secureChannel.transmit(apduChannel, getStatus);
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key
* derivation support.
*
* @return whether public key derivation is supported or not
* @throws IOException communication error
*/
public boolean getPublicKeyDerivationSupport() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 1] != 0x00;
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization
* status
*
* @return whether public key derivation is supported or not
* @throws IOException communication error
*/
public boolean getKeyInitializationStatus() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 4] != 0x00;
}
/**
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pin the pin
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse verifyPIN(String pin) throws IOException {
APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes());
return secureChannel.transmit(apduChannel, verifyPIN);
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, String pin) throws IOException {
return changePIN(pinType, pin.getBytes());
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, byte[] pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin);
return secureChannel.transmit(apduChannel, changePIN);
}
/**
* Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure
* channel and used as APDU data.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unblockPIN(String puk, String newPin) throws IOException {
APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
return secureChannel.transmit(apduChannel, unblockPIN);
}
/**
* Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of
* the command is set to LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation.
* The loaded keyset is extended and support further key derivation.
*
* @param aPrivate a private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(PrivateKey aPrivate, byte[] chainCode) throws IOException {
byte[] privateKey = ((ECPrivateKey) aPrivate).getD().toByteArray();
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
byte[] data = new byte[chainCode.length + privLen];
System.arraycopy(privateKey, privOff, data, 0, privLen);
System.arraycopy(chainCode, 0, data, privLen, chainCode.length);
return loadKey(data, LOAD_KEY_P1_SEED);
}
/**
* 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.
*
* @param ecKeyPair a key pair
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair ecKeyPair) throws IOException {
return loadKey(ecKeyPair, false, null);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value
* of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically
* to either LOAD_KEY_P1_EC or LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param keyPair a key pair
* @param omitPublicKey whether the public key is sent or not
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws IOException {
byte[] publicKey = omitPublicKey ? null : ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray();
return loadKey(publicKey, privateKey, chainCode);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is
* included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or
* LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param publicKey a raw public key
* @param privateKey a raw private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
int off = 0;
int totalLength = publicKey == null ? 0 : (publicKey.length + 2);
totalLength += (privLen + 2);
totalLength += chainCode == null ? 0 : (chainCode.length + 2);
if (totalLength > 127) {
totalLength += 3;
} else {
totalLength += 2;
}
byte[] data = new byte[totalLength];
data[off++] = (byte) 0xA1;
if (totalLength > 127) {
data[off++] = (byte) 0x81;
data[off++] = (byte) (totalLength - 3);
} else {
data[off++] = (byte) (totalLength - 2);
}
if (publicKey != null) {
data[off++] = TLV_PUB_KEY;
data[off++] = (byte) publicKey.length;
System.arraycopy(publicKey, 0, data, off, publicKey.length);
off += publicKey.length;
}
data[off++] = TLV_PRIV_KEY;
data[off++] = (byte) privLen;
System.arraycopy(privateKey, privOff, data, off, privLen);
off += privLen;
byte p1;
if (chainCode != null) {
p1 = LOAD_KEY_P1_EXT_EC;
data[off++] = (byte) TLV_CHAIN_CODE;
data[off++] = (byte) chainCode.length;
System.arraycopy(chainCode, 0, data, off, chainCode.length);
} else {
p1 = LOAD_KEY_P1_EC;
}
return loadKey(data, p1);
}
/**
* Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1.
*
* @param data key data
* @param keyType the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] data, byte keyType) throws IOException {
APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data);
return secureChannel.transmit(apduChannel, loadKey);
}
/**
* Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1.
*
* @param cs the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse generateMnemonic(int cs) throws IOException {
APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
return secureChannel.transmit(apduChannel, generateMnemonic);
}
/**
* Sends a REMOVE KEY APDU.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse removeKey() throws IOException {
APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]);
return secureChannel.transmit(apduChannel, removeKey);
}
/**
* Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form
* the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is
* generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and
* isLast will always be true at the same time.
*
* @param data the data to sign
* @param dataType the P1 parameter
* @param isFirst whether this is the first block of the command or not
* @param isLast whether this is the last block of the command or not
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws IOException {
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, dataType, p2, data);
return secureChannel.transmit(apduChannel, sign);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 and P2 parameters are forced to 0, meaning
* that the derivation starts from the master key and is non-assisted.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data) throws IOException {
return deriveKey(data, DERIVE_P1_SOURCE_MASTER, false, false);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The reset and assisted parameters are combined to
* form P1. The isPublicKey parameter is used for P2.
*
* @param data the raw key path or a public key
* @param source the source to start derivation
* @param assisted whether we are doing assisted derivation or not
* @param isPublicKey whether we are sending a public key or a key path (only make sense during assisted derivation)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data, int source, boolean assisted, boolean isPublicKey) throws IOException {
byte p1 = assisted ? DERIVE_P1_ASSISTED_MASK : 0;
p1 |= source;
byte p2 = isPublicKey ? DERIVE_P2_PUBLIC_KEY : DERIVE_P2_KEY_PATH;
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, p1, p2, data);
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
/**
* Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself
*
* @param keyPathIndex the P1 parameter
* @param publicOnly the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte keyPathIndex, boolean publicOnly) throws IOException {
byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]);
return secureChannel.transmit(apduChannel, exportKey);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param puk the PUK
* @param sharedSecret the shared secret for pairing
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length);
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
}
@@ -0,0 +1,107 @@
package im.status.keycard.installer;
import android.content.res.AssetManager;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.CardListener;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
public class ActionRunner implements CardListener {
public final static int ACTION_NONE = 0;
public final static int ACTION_INSTALL = 1;
public final static int ACTION_INSTALL_TEST = 2;
public final static int ACTION_PERFTEST = 3;
private AssetManager assets;
private String capPath;
private int requestedAction;
public ActionRunner(AssetManager assets, String capPath) {
this.assets = assets;
this.capPath = capPath;
this.requestedAction = ACTION_NONE;
}
public void requestAction(int actionRequested) {
switch(actionRequested) {
case ACTION_NONE:
Logger.i("cancelling requested action");
break;
case ACTION_INSTALL:
Logger.i("installation requested");
break;
case ACTION_INSTALL_TEST:
Logger.i("installation with test secrets requested");
break;
case ACTION_PERFTEST:
Logger.i("performance tests requested");
break;
default:
Logger.i("invalid action requested, ignoring");
return;
}
this.requestedAction = actionRequested;
}
private void perform(CardChannel ch) {
Logger.i("starting requested action");
try {
if (!ch.isConnected()) {
Logger.i("tag disconnected");
return;
}
switch (requestedAction) {
case ACTION_INSTALL:
Installer installer = new Installer(ch, this.assets, this.capPath, false);
installer.start();
break;
case ACTION_INSTALL_TEST:
installer = new Installer(ch, this.assets, this.capPath, true);
installer.start();
break;
case ACTION_PERFTEST:
PerfTest perfTest = new PerfTest(ch);
perfTest.test();
break;
default:
throw new Exception("Unknown action");
}
} catch (IOException e) {
Logger.e("IO exception: " + e.getMessage());
} catch (APDUException e) {
Logger.e("APDU exception: " + e.getMessage());
} catch (Exception e) {
Logger.e("Other exception: " + e.getMessage());
} finally {
this.requestedAction = ACTION_NONE;
}
}
@Override
public void onConnected(final CardChannel channel) {
Logger.i("card connected");
if (this.requestedAction != ACTION_NONE) {
Logger.i("waiting 2 seconds to start requested action");
new Timer().schedule(new TimerTask() {
@Override
public void run() {
perform(channel);
}
}, 2000);
} else {
Logger.i("no action requested yet");
}
}
@Override
public void onDisconnected() {
Logger.i("card disconnected");
}
}
@@ -0,0 +1,73 @@
package im.status.keycard.installer;
import android.content.res.AssetManager;
import im.status.keycard.applet.KeycardCommandSet;
import im.status.keycard.globalplatform.GlobalPlatformCommandSet;
import im.status.keycard.globalplatform.LoadCallback;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import org.bouncycastle.util.encoders.Hex;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
public class Installer {
private CardChannel plainChannel;
private AssetManager assets;
private String capPath;
private GlobalPlatformCommandSet cmdSet;
private boolean testSecrets;
public Installer(CardChannel channel, AssetManager assets, String capPath, boolean testSecrets) {
this.plainChannel = channel;
this.assets = assets;
this.capPath = capPath;
this.testSecrets = testSecrets;
}
public void start() throws IOException, APDUException, NoSuchAlgorithmException, InvalidKeySpecException {
Logger.i("installation started...");
long startTime = System.currentTimeMillis();
Logger.i("select ISD...");
cmdSet = new GlobalPlatformCommandSet(this.plainChannel);
cmdSet.select().checkOK();
Logger.i("opening secure channel...");
cmdSet.openSecureChannel();
Logger.i("deleting old version (if present)...");
cmdSet.deleteKeycardInstancesAndPackage();
Logger.i("loading package...");
cmdSet.loadKeycardPackage(this.assets.open(this.capPath), new LoadCallback() {
public void blockLoaded(int loadedBlock, int blockCount) {
Logger.i(String.format("load %d/%d...", loadedBlock, blockCount));
}
});
Logger.i("installing NDEF applet...");
cmdSet.installNDEFApplet(Hex.decode("0024d40f12616e64726f69642e636f6d3a706b67696d2e7374617475732e657468657265756d")).checkOK();
Logger.i("installing Keycard applet...");
cmdSet.installKeycardApplet().checkOK();
this.personalizeApplet();
long duration = System.currentTimeMillis() - startTime;
Logger.i(String.format("\n\ninstallation completed in %d seconds", duration / 1000));
}
private void personalizeApplet() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException {
Secrets secrets = testSecrets ? Secrets.testSecrets() : Secrets.generate();
KeycardCommandSet cmdSet = new KeycardCommandSet(this.plainChannel);
cmdSet.select().checkOK();
cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword()).checkOK();
Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword()));
}
}
@@ -1,4 +1,4 @@
package im.status.applet_installer_test.appletinstaller;
package im.status.keycard.installer;
import android.util.Log;
@@ -1,29 +1,31 @@
package im.status.applet_installer_test.appletinstaller;
package im.status.keycard.installer;
import android.content.Intent;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.nfc.NfcAdapter;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import java.security.Security;
import de.cotech.sweetspot.NfcSweetspotData;
import im.status.keycard.android.NFCCardManager;
import de.cotech.sweetspot.ShowNfcSweetspotActivity;
public class MainActivity extends AppCompatActivity implements UILogger {
static {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
private NfcAdapter nfcAdapter;
private TextView textView;
private ScrollView textViewScroll;
private Button buttonInstall;
private Button buttonInstallTest;
private Button buttonPerfTest;
private CardManager cardManager;
private Button buttonNFCSpot;
private ActionRunner actionRunner;
private NFCCardManager cardManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -33,8 +35,10 @@ public class MainActivity extends AppCompatActivity implements UILogger {
Logger.setUILogger(this);
AssetManager assets = this.getAssets();
this.cardManager = new CardManager(nfcAdapter, assets, "wallet.cap");
this.cardManager.start();
this.actionRunner = new ActionRunner(assets, "keycard.cap");
this.cardManager = new NFCCardManager();
this.cardManager.setCardListener(this.actionRunner);
cardManager.start();
textViewScroll = (ScrollView) findViewById(R.id.textViewScroll);
@@ -45,16 +49,35 @@ public class MainActivity extends AppCompatActivity implements UILogger {
buttonInstall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestAction(CardManager.ACTION_INSTALL);
requestAction(ActionRunner.ACTION_INSTALL);
}
});
buttonInstallTest = (Button) findViewById(R.id.buttonInstallTest);
buttonInstallTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestAction(ActionRunner.ACTION_INSTALL_TEST);
}
});
buttonPerfTest = (Button) findViewById(R.id.buttonPerfTest);
buttonPerfTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestAction(CardManager.ACTION_PERFTEST);
requestAction(ActionRunner.ACTION_PERFTEST);
}
});
buttonNFCSpot = (Button) findViewById(R.id.buttonNFCSpot);
buttonNFCSpot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(MainActivity.this, ShowNfcSweetspotActivity.class);
startActivity(i);
}
});
if (NfcSweetspotData.hasSweetspotData()) {
buttonNFCSpot.setEnabled(true);
}
//Logger.setUILevel(Log.INFO);
//Logger.setLevel(Log.INFO);
@@ -70,9 +93,9 @@ public class MainActivity extends AppCompatActivity implements UILogger {
}
private void requestAction(int action) {
if (this.cardManager != null) {
if (this.actionRunner != null) {
clearTextView();
this.cardManager.requestAction(action);
this.actionRunner.requestAction(action);
}
}
@@ -0,0 +1,100 @@
package im.status.keycard.installer;
import android.util.Log;
import im.status.keycard.io.CardChannel;
import im.status.keycard.applet.KeycardCommandSet;
import java.security.SecureRandom;
public class PerfTest {
private CardChannel cardChannel;
private KeycardCommandSet cmdSet;
private long openSecureChannelTime = 0;
private long loadKeysTime = 0;
private long loginTime = 0;
private long signTime = 0;
private long getStatusTime = 0;
static final String BIP44_WALLET_PATH = "m/44'/60'/0'/0/0";
public PerfTest(CardChannel cardChannel) {
this.cardChannel = cardChannel;
}
public void test() throws Exception {
cmdSet = new KeycardCommandSet(cardChannel);
cmdSet.select().checkOK();
cmdSet.autoPair(cmdSet.pairingPasswordToSecret(Secrets.testSecrets().getPairingPassword()));
openSecureChannelTime = System.currentTimeMillis();
cmdSet.autoOpenSecureChannel();
openSecureChannelTime = System.currentTimeMillis() - openSecureChannelTime;
cmdSet.verifyPIN("000000").checkOK();
cmdSet.unpairOthers(); // Recover in case of non-clean termination
Logger.i("Measuring performances. Logging disabled. Please wait");
Logger.setLevel(Log.INFO);
try {
loadKeys();
getStatus();
login();
signTransactions();
} finally {
Logger.setLevel(Log.INFO);
Logger.setUILevel(Log.INFO);
}
Logger.i("Reenabling logging.");
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
cmdSet.autoUnpair();
Logger.i("*************************************************");
Logger.i("Opening Secure Channel: " + openSecureChannelTime + "ms");
Logger.i("Derivation of m/44'/60'/0'/0/0 from master: " + loadKeysTime + "ms");
Logger.i("All following measurements are from application selection to the last needed APDU");
Logger.i("GET STATUS: " + getStatusTime + "ms");
Logger.i("Login: " + loginTime + "ms");
Logger.i("Transaction signature: " + signTime + "ms");
}
private void getStatus() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK();
getStatusTime = System.currentTimeMillis() - time;
}
private void login() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
cmdSet.exportKey("m/43'/60'/1581'/0'/0",false, false).checkOK();
cmdSet.exportKey("m/43'/60'/1581'/1'/0",false, false).checkOK();
loginTime = System.currentTimeMillis() - time;
}
private void loadKeys() throws Exception {
byte[] seed = new byte[64];
new SecureRandom().nextBytes(seed);
cmdSet.loadKey(seed).checkOK();
long time = System.currentTimeMillis();
cmdSet.deriveKey(BIP44_WALLET_PATH).checkOK();
loadKeysTime = System.currentTimeMillis() - time;
}
private void signTransactions() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes()).checkOK();
signTime = System.currentTimeMillis() - time;
}
}
@@ -0,0 +1,60 @@
package im.status.keycard.installer;
import android.util.Base64;
import im.status.keycard.globalplatform.Crypto;
import java.security.SecureRandom;
import static android.util.Base64.NO_PADDING;
import static im.status.keycard.globalplatform.Crypto.randomLong;
public class Secrets {
private String pin;
private String puk;
private String pairingPassword;
public Secrets(String pin, String puk, String pairingPassword) {
this.pin = pin;
this.puk = puk;
this.pairingPassword = pairingPassword;
}
public static Secrets generate() {
String pairingPassword = randomToken(12);
long pinNumber = randomLong(Crypto.PIN_BOUND);
long pukNumber = randomLong(Crypto.PUK_BOUND);
String pin = String.format("%06d", pinNumber);
String puk = String.format("%012d", pukNumber);
return new Secrets(pin, puk, pairingPassword);
}
public static Secrets testSecrets() {
String pairingPassword = "KeycardTest";
return new Secrets("000000", "123456789012", pairingPassword);
}
public String getPin() {
return pin;
}
public String getPuk() {
return puk;
}
public String getPairingPassword() {
return pairingPassword;
}
public static byte[] randomBytes(int length) {
SecureRandom random = new SecureRandom();
byte data[] = new byte[length];
random.nextBytes(data);
return data;
}
public static String randomToken(int length) {
return Base64.encodeToString(randomBytes(length), NO_PADDING);
}
}
+16 -4
View File
@@ -24,15 +24,27 @@
</ScrollView>
<Button
android:id="@+id/buttonInstall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Install" />
android:id="@+id/buttonInstall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Install Secure"/>
<Button
android:id="@+id/buttonInstallTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Install with test PIN/PUK"/>
<Button
android:id="@+id/buttonPerfTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Perf Test" />
<Button
android:id="@+id/buttonNFCSpot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="NFC Spot"
android:enabled="false" />
</LinearLayout>
+1 -1
View File
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">AppletInstaller</string>
<string name="app_name">KeycardInstaller</string>
</resources>
@@ -1,28 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import org.junit.Test;
import java.io.IOException;
import static org.junit.Assert.*;
public class APDUCommandTest {
@Test
public void serialize() throws IOException {
int cla = 0x80;
int ins = 0x50;
int p1 = 0;
int p2 = 0;
byte[] data = HexUtils.hexStringToByteArray("84762336c5187fe8");
APDUCommand c = new APDUCommand(cla, ins, p1, p2, (byte[])data, true);
String expected = "805000000884762336C5187FE800";
String actual = HexUtils.byteArrayToHexString(c.serialize());
assertEquals(expected, actual);
c = new APDUCommand(cla, ins, p1, p2, (byte[])data);
expected = "805000000884762336C5187FE8";
actual = HexUtils.byteArrayToHexString(c.serialize());
assertEquals(expected, actual);
}
}
@@ -1,21 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import org.junit.Test;
import static org.junit.Assert.*;
public class APDUResponseTest {
@Test
public void parsing() {
byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a69000");
APDUResponse resp = new APDUResponse(apdu);
assertEquals(0x9000, resp.getSw());
assertEquals(0x90, resp.getSw1());
assertEquals(0x00, resp.getSw2());
String expected = "000002650183039536622002003B5E508F751C0AF3016E3FBC23D3A6";
assertEquals(expected, HexUtils.byteArrayToHexString(resp.getData()));
assertTrue(resp.isOK());
}
}
@@ -1,54 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import org.junit.Test;
import java.io.IOException;
import static org.junit.Assert.*;
public class APDUWrapperTest {
@Test
public void wrap() throws IOException {
byte[] macKeyData = HexUtils.hexStringToByteArray("2983BA77D709C2DAA1E6000ABCCAC951");
byte[] data = HexUtils.hexStringToByteArray("1d4de92eaf7a2c9f");
APDUCommand cmd = new APDUCommand(0x84, 0x82, 0x01, 0x00, data);
APDUWrapper w = new APDUWrapper(macKeyData);
// check null icv
assertEquals(HexUtils.byteArrayToHexString(Crypto.NullBytes8), HexUtils.byteArrayToHexString(w.getICV()));
APDUCommand wrapped = w.wrap(cmd);
byte[] result = wrapped.serialize();
String expected = "84820100101D4DE92EAF7A2C9F8F9B0DF681C1D3EC";
assertEquals(expected, HexUtils.byteArrayToHexString(result));
// second command
// check icv from previous mac
assertEquals("8F9B0DF681C1D3EC", HexUtils.byteArrayToHexString(w.getICV()));
data = HexUtils.hexStringToByteArray("4F00");
cmd = new APDUCommand(0x80, 0xF2, 0x80, 0x02, data, true);
wrapped = w.wrap(cmd);
result = wrapped.serialize();
expected = "84F280020A4F0030F149209E17B39700";
assertEquals(expected, HexUtils.byteArrayToHexString(result));
// third command
// check icv from previous mac
assertEquals("30F149209E17B397", HexUtils.byteArrayToHexString(w.getICV()));
data = HexUtils.hexStringToByteArray("4F00");
cmd = new APDUCommand(0x80, 0xF2, 0x40, 0x02, data, true);
wrapped = w.wrap(cmd);
result = wrapped.serialize();
expected = "84F240020A4F000D9B2D4E4365B5BD00";
assertEquals(expected, HexUtils.byteArrayToHexString(result));
}
}
@@ -1,63 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import org.junit.Test;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import static org.junit.Assert.*;
import im.status.applet_installer_test.appletinstaller.apducommands.InitializeUpdate;
public class CryptoTest {
@Test
public void deriveKey() {
byte[] cardKey = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
byte[] seq = HexUtils.hexStringToByteArray("0065");
byte[] encKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_ENC);
String expectedEncKey = "85E72AAF47874218A202BF5EF891DD21";
assertEquals(expectedEncKey, HexUtils.byteArrayToHexString(encKey));
byte[] macKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_MAC);
String expectedMacKey = "309CF99E164F3A97F3E5017FF540A79F";
assertEquals(expectedMacKey, HexUtils.byteArrayToHexString(macKey));
byte[] dekKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_DEK);
String expectedDekKey = "93D08F8025242C4D775D69B9F16C939B";
assertEquals(expectedDekKey, HexUtils.byteArrayToHexString(dekKey));
}
@Test
public void appendDESPadding() {
byte[] data = HexUtils.hexStringToByteArray("AABB");
byte[] result = Crypto.appendDESPadding(data);
String expected = "AABB800000000000";
assertEquals(expected, HexUtils.byteArrayToHexString(result));
}
@Test
public void verifyCryptogram() {
byte[] encKey = HexUtils.hexStringToByteArray("16B5867FF50BE7239C2BF1245B83A362");
byte[] hostChallenge = HexUtils.hexStringToByteArray("32da078d7aac1cff");
byte[] cardChallenge = HexUtils.hexStringToByteArray("007284f64a7d6465");
byte[] cardCryptogram = HexUtils.hexStringToByteArray("05c4bb8a86014e22");
assertTrue(Crypto.verifyCryptogram(encKey, hostChallenge, cardChallenge, cardCryptogram));
}
@Test
public void macFull3des() {
byte[] key = HexUtils.hexStringToByteArray("5b02e75ad63190aece0622936f11abab");
byte[] data = HexUtils.hexStringToByteArray("8482010010810b098a8fbb88da800000");
String expected = "5271D7174A5A166A";
byte[] result = Crypto.macFull3des(key, data, Crypto.NullBytes8);
assertEquals(expected, HexUtils.byteArrayToHexString(result));
}
@Test
public void generatePairingKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
String pairing = "zoynhcfz1xJjYqxO";
byte[] key = Crypto.generatePairingKey(pairing.toCharArray());
String expected = "BF8D606E2FE9292B633DF5E31563AF88928EEBB71FDBCBEF6CADECAA00D7874F";
assertEquals(expected, HexUtils.byteArrayToHexString(key));
}
}
@@ -1,46 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.IOException;
import org.junit.Test;
import static org.junit.Assert.*;
public class TlvTest {
@Test
public void find() throws IOException {
byte[] data = HexUtils.hexStringToByteArray("C102BBCCC203DDEE11C30201");
Tlv tlv = new Tlv(data);
Tlv child1 = tlv.find((byte) 0xC1);
String expected = "BBCC";
assertEquals(expected, HexUtils.byteArrayToHexString(child1.getValue()));
Tlv child2 = tlv.find((byte) 0xC2);
expected = "DDEE11";
assertEquals(expected, HexUtils.byteArrayToHexString(child2.getValue()));
// tag exists, length is bigger than available data
assertNull(tlv.find((byte) 0xC3));
// not found
assertNull(tlv.find((byte) 0xFF));
// not found
assertNull(child2.find((byte) 0xFF));
}
@Test
public void findAID() throws IOException {
byte[] data = HexUtils.hexStringToByteArray("6f5c8408a000000151000000a550734a06072a864886fc6b01600c060a2a864886fc6b02020201630906072a864886fc6b03640b06092a864886fc6b040255650b06092a864886fc6b020103660c060a2b060104012a026e01039f6501ff9000");
Tlv tlv = new Tlv(data);
Tlv child1 = tlv.find((byte) 0x6F);
assertNotNull(child1);
Tlv child2 = child1.find((byte) 0x84);
assertNotNull(child2);
String expected = "A000000151000000";
assertEquals(expected, HexUtils.byteArrayToHexString(child2.getValue()));
}
}
@@ -1,21 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class DeleteTest {
@Test
public void getCommand() throws IOException {
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
Delete delete = new Delete(aid);
String expected = "80E400000E4F0C53746174757357616C6C6574";
byte[] apdu = delete.getCommand().serialize();
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}
@@ -1,38 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class ExternalAuthenticateTest {
@Test
public void getHostCryptogram() {
byte[] encKeyData = HexUtils.hexStringToByteArray("0EF72A1065236DD6CAC718D5E3F379A4");
byte[] cardChallenge = HexUtils.hexStringToByteArray("0076a6c0d55e9535");
byte[] hostChallenge = HexUtils.hexStringToByteArray("266195e638da1b95");
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
String expectedHostCryptogram = "45A5F48DAE68203C";
byte[] hostCryptogram = auth.getHostCryptogram();
assertEquals(expectedHostCryptogram, HexUtils.byteArrayToHexString(hostCryptogram));
}
@Test
public void getCommand() throws IOException {
byte[] encKeyData = HexUtils.hexStringToByteArray("8D289AFE0AB9C45B1C76DEEA182966F4");
byte[] cardChallenge = HexUtils.hexStringToByteArray("000f3fd65d4d6e45");
byte[] hostChallenge = HexUtils.hexStringToByteArray("cf307b6719bf224d");
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
String expectedAPDU = "84820100087702AC6CE46A47F0";
byte[] apdu = auth.getCommand().serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
}
@@ -1,62 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import im.status.applet_installer_test.appletinstaller.Keys;
import static org.junit.Assert.*;
public class InitializeUpdateTest {
@Test
public void getCommand() throws IOException {
byte[] challenge = HexUtils.hexStringToByteArray("2d315d5ffc616d10");
InitializeUpdate init = new InitializeUpdate(challenge);
APDUCommand cmd = init.getCommand();
assertEquals(0x80, cmd.getCla());
assertEquals(0x50, cmd.getIns());
assertEquals(0, cmd.getP1());
assertEquals(0, cmd.getP2());
assertEquals(challenge, cmd.getData());
String expectedAPDU = "80500000082D315D5FFC616D1000";
byte[] apdu = cmd.serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
@Test
public void validateResponse_BadResponse() throws APDUException {
byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a66982");
APDUResponse resp = new APDUResponse(apdu);
byte[] challenge = InitializeUpdate.generateChallenge();
InitializeUpdate init = new InitializeUpdate(challenge);
try {
init.verifyResponse(new Keys(new byte[]{}, new byte[]{}), resp);
fail("expected APDUException to be thrown");
} catch (APDUException e) {
assertEquals(0x6982, e.sw);
}
}
//TODO: reimplement test
//@Test
//public void validateResponse_GoodResponse() throws APDUException {
// byte[] encKey = HexUtils.hexStringToByteArray("16B5867FF50BE7239C2BF1245B83A362");
// byte[] challenge = HexUtils.hexStringToByteArray("f0467f908e5ca23f");
// InitializeUpdate init = new InitializeUpdate(challenge);
// byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002000de9c62ba1c4c8e55fcb91b6654ce49000");
// APDUResponse resp = new APDUResponse(apdu);
// init.verifyResponse(new Keys(), resp);
//}
}
@@ -1,26 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUCommandTest;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class InstallForInstallTest {
@Test
public void forInstall() throws IOException {
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] params = HexUtils.hexStringToByteArray("AABBCC");
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params);
APDUCommand cmd = install.getCommand();
byte[] apdu = cmd.serialize();
String expected = "80E60C00360C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C6574417070010005C903AABBCC00";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}
@@ -1,24 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class InstallForLoadTest {
@Test
public void forLoad() throws IOException {
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] sdaid = HexUtils.hexStringToByteArray("A000000151000000");
InstallForLoad install = new InstallForLoad(aid, sdaid);
APDUCommand cmd = install.getCommand();
byte[] apdu = cmd.serialize();
String expected = "80E60200190C53746174757357616C6C657408A000000151000000000000";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}
@@ -1,52 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class LoadTest {
@Test
public void getCommand() throws IOException {
//URL url = this.getClass().getClassLoader().getResource("wallet.cap");
//Load load = new Load(url.getPath());
//ArrayList<APDUCommand> commands = new ArrayList<APDUCommand>();
//APDUCommand cmd;
//while((cmd = load.getCommand()) != null) {
// commands.add(cmd);
//}
//assertEquals(31, commands.size());
//// Command 1
//cmd = commands.get(0);
//assertEquals(0, cmd.getP1());
//assertEquals(0, cmd.getP2());
//String expectedData = "C4821D74010027DECAFFED02020401010C53746174757357616C6C657410696D2F7374617475732F77616C6C6574020021002700210013002902B600401581015D0301000006EB372C0024000A013504010004002904000107A0000000620001050107A0000000620102050107A0000000620101050107A0000000620201030013010F53746174757357616C6C65744170700937060040000000800000FF000100000000800000FF00010000000080000D000A010A000004EB05A005F307140739080C08D608EB0908090D008203160010070100000A7B07158106005A801A0076003003808009038B00300457800904620030051F8010";
//byte[] data = cmd.getData();
//assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
//// Command 2
//cmd = commands.get(1);
//assertEquals(0, cmd.getP1());
//assertEquals(1, cmd.getP2());
//expectedData = "053100440A9380AB0B4000750EBF80930F5400300110188C002F7A0302058D00327F003307038D00347F003506038D00377F00381006038D00347F003B032F101B038D00407F004110141020038D0042940000437F005E700B2C017F00411100802F10651C41048D005F7F00607A0861032906181D2510805310806B1E7B00601606590601033816061A7B006016068E03006113412906702B1B7B0060038E03006213044329067B0060037B00601606250453600506700305381606054704412906181D7B00601606078D006329061504160510207B00600316067B006016068D00647B006016067B0065038D0066620403781A7B0060";
//data = cmd.getData();
//assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
//// Last command
//cmd = commands.get(30);
//assertEquals(0x80, cmd.getP1());
//assertEquals(30, cmd.getP2());
//expectedData = "080707080C08090A04050A0A06070706081A07085029031512201209190C0B0B0503060D09030E0B0C0C080A0A0603070706104622070914240A1611130D080B0F0A0D10110905040B2E271205030E0D0D0D0D0807140808030E1E10050321032C2A070606080D140C2723180B081D0A0707060811030D0407070608201B07091408252C2E39";
//data = cmd.getData();
//assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
}
}
@@ -1,20 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class SelectTest {
@Test
public void getCode() throws IOException {
Select s = new Select(new byte[0]);
byte[] apdu = s.getCommand().serialize();
String expected = "00A4040000";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}
@@ -1,21 +0,0 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUWrapper;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class StatusTest {
@Test
public void getCommand() throws IOException {
Status status = new Status(Status.P1_ISSUER_SECURITY_DOMAIN);
String expectedAPDU = "80F28002024F0000";
byte[] apdu = status.getCommand().serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
}
+1
View File
@@ -19,6 +19,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
+1
View File
@@ -1 +1,2 @@
include ':app'