Compare commits

Author SHA1 Message Date
Michele Balistreri 341c5cbe1d simplify NDEF configuration 2018-11-13 14:27:26 +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
Michele Balistreri ea170172b5 support secret personalization separate from INSTALL command 2018-10-10 15:06:32 +02:00
Andrea Franz 2fdb108303 typo - number of steps 2018-09-26 18:46:14 +02:00
Andrea Franz c7fc137724 fix delete test 2018-09-26 15:56:32 +02:00
Andrea Franz 97773b6c37 delete applet before installing. in 2 steps, applet and then pkg 2018-09-26 15:53:44 +02:00
Michele Balistreri 05780c1f9c update cap file 2018-09-26 15:23:50 +02:00
Michele Balistreri 37b7b9ebc8 use unassisted key derivation 2018-09-24 16:24:06 +03:00
Andrea Franz 9c1adc9507 add log level for console and UI logs 2018-09-19 13:58:17 +02:00
Michele Balistreri 99c277021c update wallet file 2018-09-18 11:32:20 +03:00
Andrea Franz 6b493118c5 update demo gif 2018-09-17 12:32:35 +02:00
Andrea Franz 535630f2ae Update README.md 2018-09-16 12:40:26 +02:00
Andrea Franz 7088d3acbf add README 2018-09-16 12:37:21 +02:00
Andrea Franz 9470bc3314 add demo gif 2018-09-16 12:23:33 +02:00
Michele Balistreri 68fd877214 use pairing key generated according to PBKDF2 2018-09-14 11:34:59 +03:00
Andrea Franz cf41d9d7ff installer generates secrets 2018-09-12 17:11:24 +02:00
Andrea Franz cdaf79eebe add Secrets class 2018-09-12 17:10:16 +02:00
Andrea Franz 746ac5c8f0 add generatePairingKey 2018-09-12 12:22:27 +02:00
Andrea Franz 93162a4c1e add SW_CONDITIONS_OF_USE_NOT_SATISFIED constant 2018-09-12 10:44:22 +02:00
Michele Balistreri f1f5e8bd99 add more measurements 2018-09-11 11:47:11 +03:00
Michele Balistreri 969c33aa20 integrate performance tests back in the code 2018-09-11 10:14:51 +03:00
Andrea Franz c86ca40480 add SW_REFERENCED_DATA_NOT_FOUND constant 2018-09-11 02:48:04 +02:00
Andrea Franz 0eb18e79f1 temporarily remove delete command 2018-09-10 18:07:18 +02:00
Andrea Franz 6eb571157f show installation duration in seconds 2018-09-10 16:21:15 +02:00
Andrea Franz 27fe6df4d0 run installation async 2018-09-10 16:00:11 +02:00
Andrea Franz 703c8ea860 delete applet before installing 2018-09-10 15:59:11 +02:00
Andrea Franz 79c3834859 make card manager work in a different thread 2018-09-10 15:58:06 +02:00
Andrea Franz 3bc62e8e49 improve UI buttons 2018-09-10 15:57:46 +02:00
Andrea Franz 4309ef5792 temporarily remove Load command tests 2018-09-10 15:57:18 +02:00
Andrea Franz 61dbec1a7f add missing Delete command code 2018-09-10 15:56:47 +02:00
Andrea Franz 75ec4d0723 remove secure channel logs from UI 2018-09-10 15:55:09 +02:00
Andrea Franz 8678b76612 add Dele command and test 2018-09-10 12:48:20 +02:00
Andrea Franz ee5c3709c6 add CardManager thread 2018-09-07 19:47:40 +02:00
Andrea Franz 74b0ea6a07 make textview text selectable 2018-09-07 16:38:19 +02:00
Andrea Franz d56d2df760 remove old cap file 2018-09-07 15:19:19 +02:00
Bitgamma 8480ff4173 Merge pull request #1 from status-im/performance-tests
Performance tests
2018-09-07 14:47:00 +03:00
42 changed files with 589 additions and 1467 deletions
+11 -2
View File
@@ -5,22 +5,31 @@
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="5">
<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="4">
<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>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
# smartcard-cap-installer-test
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.
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`.
[Download APK](https://github.com/status-im/smartcard-cap-installer-test/releases/download/0.0.1/app-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)
+3 -2
View File
@@ -6,8 +6,8 @@ android {
applicationId "im.status.applet_installer_test.appletinstaller"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
versionCode 5
versionName "0.0.5"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -24,6 +24,7 @@ dependencies {
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:hardwallet-lite-android:29bf055'
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'
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,67 +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;
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("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,6 +1,8 @@
package im.status.applet_installer_test.appletinstaller;
import im.status.hardwallet_lite_android.io.APDUCommand;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
@@ -0,0 +1,104 @@
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 im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.hardwallet_lite_android.io.OnCardConnectedListener;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
public class ActionRunner implements OnCardConnectedListener {
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) {
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");
}
}
}
@@ -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.log(String.format("COMMAND %s %n", HexUtils.byteArrayToHexString(apdu)));
byte[] resp = this.isoDep.transceive(apdu);
Logger.log(String.format("RESPONSE %s %n", HexUtils.byteArrayToHexString(resp)));
return new APDUResponse(resp);
}
}
@@ -1,29 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import android.content.res.AssetManager;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import java.io.IOException;
public class CardManager {
private Tag tag;
private IsoDep isoDep;
public CardManager(Tag tag) {
this.tag = tag;
}
public void connect() throws IOException {
this.isoDep = IsoDep.get(tag);
this.isoDep.setTimeout(120000);
Logger.log("Is connected: " + this.isoDep.isConnected());
this.isoDep.connect();
}
public void install(AssetManager assets, String capPath) throws IOException, APDUException {
CardChannel ch = new CardChannel(this.isoDep);
Installer installer = new Installer(ch, assets, capPath);
installer.start();
}
}
@@ -1,7 +0,0 @@
package im.status.applet_installer_test.appletinstaller;
import java.io.IOException;
public interface Channel {
APDUResponse send(APDUCommand cmd) throws IOException;
}
@@ -4,19 +4,29 @@ 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 android.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;
import static android.util.Base64.NO_PADDING;
public class Crypto {
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
public static byte[] deriveKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
byte[] key24 = resizeKey24(cardKey);
@@ -125,4 +135,31 @@ public class Crypto {
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) {
return Base64.encodeToString(randomBytes(length),NO_PADDING);
}
}
@@ -4,38 +4,43 @@ import android.content.res.AssetManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import im.status.applet_installer_test.appletinstaller.apducommands.ExternalAuthenticate;
import im.status.applet_installer_test.appletinstaller.apducommands.InitializeUpdate;
import im.status.applet_installer_test.appletinstaller.apducommands.InstallForInstall;
import im.status.applet_installer_test.appletinstaller.apducommands.InstallForLoad;
import im.status.applet_installer_test.appletinstaller.apducommands.Load;
import im.status.applet_installer_test.appletinstaller.apducommands.Select;
import im.status.applet_installer_test.appletinstaller.apducommands.Status;
import im.status.applet_installer_test.appletinstaller.apducommands.*;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet;
public class Installer {
private Channel channel;
private CardChannel plainChannel;
private SecureChannel channel;
private Keys cardKeys;
private AssetManager assets;
private String capPath;
private boolean testSecrets;
static final byte[] cardKeyData = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
public Installer(Channel channel, AssetManager assets, String capPath) {
this.channel = channel;
public Installer(CardChannel channel, AssetManager assets, String capPath, boolean testSecrets) {
this.plainChannel = channel;
this.cardKeys = new Keys(cardKeyData, cardKeyData);
this.assets = assets;
this.capPath = capPath;
this.testSecrets = testSecrets;
}
public void start() throws IOException, APDUException {
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.log("sdaid: " + HexUtils.byteArrayToHexString(sdaid));
Logger.d("sdaid: " + HexUtils.byteArrayToHexString(sdaid));
byte[] hostChallenge = InitializeUpdate.generateChallenge();
InitializeUpdate init = new InitializeUpdate(hostChallenge);
@@ -44,7 +49,7 @@ public class Installer {
Session session = init.verifyResponse(this.cardKeys, resp);
Keys sessionKeys = session.getKeys();
this.channel = new SecureChannel(this.channel, sessionKeys);
this.channel = new SecureChannel(this.plainChannel, sessionKeys);
ExternalAuthenticate auth = new ExternalAuthenticate(sessionKeys.getEncKeyData(), session.getCardChallenge(), hostChallenge);
resp = this.send("external auth", auth.getCommand());
@@ -55,36 +60,69 @@ public class Installer {
//Status status = new Status(Status.P1_EXECUTABLE_LOAD_FILES_AND_MODULES);
//resp = this.send("status", status.getCommand());
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
InstallForLoad preLoad = new InstallForLoad(aid, sdaid);
this.send("install for load", preLoad.getCommand());
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] walletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] ndefAppletAID = HexUtils.hexStringToByteArray("53746174757357616C6C65744E4643");
byte[] ndefInstanceAID = HexUtils.hexStringToByteArray("D2760000850101");
Delete deleteNDEFApplet = new Delete(ndefInstanceAID);
Logger.i("sending delete (NDEF applet)");
this.channel.send(deleteNDEFApplet.getCommand());
Delete deleteApplet = new Delete(walletAID);
Logger.i("sending delete (applet)");
this.channel.send(deleteApplet.getCommand());
Delete deletePkg = new Delete(packageAID);
Logger.i("sending delete (pkg)");
this.channel.send(deletePkg.getCommand());
InstallForLoad preLoad = new InstallForLoad(packageAID, 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);
Logger.log("---- Before");
APDUCommand loadCmd;
while((loadCmd = load.getCommand()) != null) {
Logger.log("sending load command " + load.getCount());
this.send("load " + load.getCount(), loadCmd);
this.send("load " + load.getCount() + "/35", loadCmd);
}
Logger.log("---- After");
InstallForInstall installNDEF = new InstallForInstall(packageAID, ndefAppletAID, ndefInstanceAID, HexUtils.hexStringToByteArray("0024d40f12616e64726f69642e636f6d3a706b67696d2e7374617475732e657468657265756d"));
this.send("perform and make selectable (NDEF)", installNDEF.getCommand());
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
InstallForInstall install = new InstallForInstall(packageAID, walletAID, walletAID, new byte[0]);
this.send("perform and make selectable (wallet)", install.getCommand());
byte[] params = HexUtils.hexStringToByteArray("3236393732333032383339318bfb5c8ea8b78a84b9efbfbc897d80312e71e559145947f447d8b6d0d9fcdb55");
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params);
this.send("install and make selectable", install.getCommand());
personalizeApplet();
long duration = System.currentTimeMillis() - startTime;
Logger.i(String.format("installation completed in %d seconds", duration / 1000));
}
private void personalizeApplet() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException {
Secrets secrets = testSecrets ? Secrets.testSecrets() : Secrets.generate();
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(this.plainChannel);
cmdSet.select().checkOK();
cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingToken()).checkOK();
Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s\nPairing token: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword(), HexUtils.byteArrayToHexString(secrets.getPairingToken())));
}
private APDUResponse send(String description, APDUCommand cmd) throws IOException, APDUException {
Logger.log("sending command " + description);
APDUResponse resp = this.channel.send(cmd);
Logger.d("sending command " + description);
APDUResponse resp = this.channel == null ? this.plainChannel.send(cmd) : 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);
}
@@ -2,27 +2,61 @@ package im.status.applet_installer_test.appletinstaller;
import android.util.Log;
interface LogListener {
interface UILogger {
public void log(String m);
}
public class Logger {
private static LogListener listener;
private static UILogger uiLogger;
private static int Level = Log.VERBOSE;
private static int UILevel = Log.VERBOSE;
public static void setListener(LogListener l) {
listener = l;
public static void setUILogger(UILogger l) {
uiLogger = l;
}
public static void log(String m) {
if (m != null) {
Log.d("installer-debug", m);
if (listener != null) {
listener.log(m);
public static void setLevel(int level) {
Level = level;
}
public static void setUILevel(int level) {
UILevel = level;
}
public static void log(int _level, String m, boolean showInUI) {
if (m != null && _level >= Level) {
Log.println(_level, "installer-debug", m);
if (showInUI && uiLogger != null && _level >= UILevel) {
uiLogger.log(m);
}
}
}
public static void log(byte[] m) {
log(HexUtils.byteArrayToHexString(m));
public static void log(int level, String m) {
log(level, m, true);
}
public static void d(String m, boolean showInUI) {
log(Log.DEBUG, m, showInUI);
}
public static void d(String m) {
d(m, true);
}
public static void i(String m, boolean showInUI) {
log(Log.INFO, m, showInUI);
}
public static void i(String m) {
i(m, true);
}
public static void e(String m, boolean showInUI) {
log(Log.ERROR, m, showInUI);
}
public static void e(String m) {
e(m, true);
}
}
@@ -4,68 +4,72 @@ import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
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.io.IOException;
import im.status.hardwallet_lite_android.io.CardManager;
import java.security.Security;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.security.SecureRandom;
public class MainActivity extends AppCompatActivity implements NfcAdapter.ReaderCallback, LogListener {
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 Tag tag;
private boolean installationAttempted;
private ActionRunner actionRunner;
private CardManager cardManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
Logger.setListener(this);
Logger.setUILogger(this);
AssetManager assets = this.getAssets();
this.actionRunner = new ActionRunner(assets, "wallet.cap");
this.cardManager = new CardManager();
this.cardManager.setOnCardConnectedListener(this.actionRunner);
cardManager.start();
textViewScroll = (ScrollView) findViewById(R.id.textViewScroll);
textView = (TextView) findViewById(R.id.textView);
textView.setMovementMethod(new ScrollingMovementMethod());
buttonInstall = (Button) findViewById(R.id.buttonInstall);
buttonInstall.setEnabled(false);
buttonInstall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
disableButtons();
try {
install();
} catch (APDUException e) {
logException(e);
} catch (IOException e) {
logException(e);
}
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.setEnabled(false);
buttonPerfTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
disableButtons();
try {
perfTest();
} catch (Exception e) {
Logger.log(e.getMessage());
}
requestAction(ActionRunner.ACTION_PERFTEST);
}
});
//Logger.setUILevel(Log.INFO);
//Logger.setLevel(Log.INFO);
}
private void logException(Exception e) {
@@ -74,35 +78,21 @@ public class MainActivity extends AppCompatActivity implements NfcAdapter.Reader
msg = "exception without message";
}
Logger.log("exception: " + msg);
Logger.e("exception: " + msg);
}
public void install() throws IOException, APDUException {
//if (installationAttempted) {
// throw new APDUException("installation already attempted");
//}
installationAttempted = true;
CardManager cm = new CardManager(tag);
cm.connect();
AssetManager assets = this.getAssets();
cm.install(assets, "wallet.cap");
}
public void perfTest() throws Exception {
Logger.log("Starting performance tests");
PerfTest pf = new PerfTest(tag);
pf.connect();
pf.test();
private void requestAction(int action) {
if (this.actionRunner != null) {
clearTextView();
this.actionRunner.requestAction(action);
}
}
@Override
public void onResume() {
super.onResume();
if (nfcAdapter != null) {
nfcAdapter.enableReaderMode(this, this,
nfcAdapter.enableReaderMode(this, this.cardManager,
NfcAdapter.FLAG_READER_NFC_A |
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null);
@@ -117,51 +107,22 @@ public class MainActivity extends AppCompatActivity implements NfcAdapter.Reader
}
}
@Override
public void onTagDiscovered(Tag tag) {
try {
start(tag);
} catch (final IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.append("\nexception: " + e.getMessage());
}
});
}
}
private void start(Tag tag) throws IOException {
this.tag = tag;
Logger.log("--------------------------\ntag found");
this.enableButtons();
}
public void enableButtons() {
runOnUiThread(new Runnable() {
@Override
public void run() {
buttonInstall.setEnabled(true);
buttonPerfTest.setEnabled(true);
}
});
}
public void disableButtons() {
runOnUiThread(new Runnable() {
@Override
public void run() {
buttonInstall.setEnabled(false);
buttonPerfTest.setEnabled(false);
}
});
}
public void log(final String s) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.append(s + "\n");
//textViewScroll.fullScroll(ScrollView.FOCUS_DOWN);
textViewScroll.scrollTo(0, textView.getBottom());
}
});
}
public void clearTextView() {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("");
}
});
}
@@ -1,36 +1,30 @@
package im.status.applet_installer_test.appletinstaller;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
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 android.util.Log;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet;
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 Tag tag;
private IsoDep isoDep;
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;
@@ -39,60 +33,68 @@ public class PerfTest {
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
}
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_WHISPER = 0x01;
static final byte EXPORT_KEY_P1_DATABASE = 0x02;
// 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 = new byte[] { (byte) 0x17, (byte) 0x83, (byte) 0x81, (byte) 0xc5, (byte) 0xe8, (byte) 0xd3, (byte) 0x24, (byte) 0xbe, (byte) 0xd4, (byte) 0x03, (byte) 0x3d, (byte) 0x14, (byte) 0xe1, (byte) 0xe1, (byte) 0xfd, (byte) 0xca, (byte) 0xaa, (byte) 0xdb, (byte) 0x74, (byte) 0x80, (byte) 0x38, (byte) 0x69, (byte) 0xbe, (byte) 0xe9, (byte) 0xf7, (byte) 0xa1, (byte) 0x0b, (byte) 0x1b, (byte) 0x71, (byte) 0x08, (byte) 0xed, (byte) 0x53 };
public PerfTest(Tag tag) {
this.tag = tag;
}
public void connect() throws IOException {
this.isoDep = IsoDep.get(tag);
this.isoDep.setTimeout(10000);
this.isoDep.connect();
public PerfTest(CardChannel cardChannel) {
this.cardChannel = cardChannel;
}
public void test() throws Exception {
CardChannel apduChannel = new CardChannel(this.isoDep);
cmdSet = new WalletAppletCommandSet(apduChannel);
byte[] keyData = extractPublicKeyFromSelect(cmdSet.select().getData());
secureChannel = new SecureChannelSession(keyData);
cmdSet.setSecureChannel(secureChannel);
cmdSet.autoPair(SHARED_SECRET);
cmdSet = new WalletAppletCommandSet(cardChannel);
cmdSet.select().checkOK();
cmdSet.autoPair(Secrets.testSecrets().getPairingToken());
openSecureChannelTime = System.currentTimeMillis();
cmdSet.autoOpenSecureChannel();
openSecureChannelTime = System.currentTimeMillis() - openSecureChannelTime;
cmdSet.verifyPIN("000000").checkOK();
cmdSet.unpairOthers(); // Recover in case of non-clean termination
loadKeys();
measureLogin();
cmdSet.select();
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 (after login): " + signTime + "ms");
Logger.i("Transaction signature (subsequent): " + (signTime - deriveKeyFromParent) + "ms");
}
private void measureLogin() throws Exception {
private void getStatus() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select();
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.getStatus(WalletAppletCommandSet.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();
APDUResponse resp = cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x00}, DERIVE_P1_SOURCE_PARENT, true, false).checkOK();
cmdSet.deriveKey(derivePublicKey(resp.getData()), DERIVE_P1_SOURCE_CURRENT, true, true).checkOK();
cmdSet.exportKey(EXPORT_KEY_P1_WHISPER, false).checkOK();
resp = cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x01}, DERIVE_P1_SOURCE_PARENT, true, false).checkOK();
cmdSet.deriveKey(derivePublicKey(resp.getData()), DERIVE_P1_SOURCE_CURRENT, true, true).checkOK();
cmdSet.exportKey(EXPORT_KEY_P1_DATABASE, false).checkOK();
time = System.currentTimeMillis() - time;
Logger.log("Total login time: " + time + "ms");
cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x00}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK();
cmdSet.exportKey(WalletAppletCommandSet.EXPORT_KEY_P1_HIGH, false).checkOK();
cmdSet.deriveKey(new byte[] { (byte) 0xC0, 0x00, 0x00, 0x01}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK();
cmdSet.exportKey(WalletAppletCommandSet.EXPORT_KEY_P1_HIGH, false).checkOK();
loginTime = System.currentTimeMillis() - time;
}
private void loadKeys() throws Exception {
@@ -104,13 +106,20 @@ public class PerfTest {
cmdSet.loadKey(keyPair, false, chainCode).checkOK();
long time = System.currentTimeMillis();
for (int i = 0; i < BIP44_PATH.length; i += 4) {
APDUResponse resp = cmdSet.deriveKey(Arrays.copyOfRange(BIP44_PATH, i, i+4), DERIVE_P1_SOURCE_CURRENT, true, false).checkOK();
cmdSet.deriveKey(derivePublicKey(resp.getData()), DERIVE_P1_SOURCE_CURRENT, true, true).checkOK();
}
time = System.currentTimeMillis() - time;
cmdSet.deriveKey(BIP44_PATH, WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT).checkOK();
loadKeysTime = System.currentTimeMillis() - time;
}
Logger.log("Total time for m/44'/60'/0'/0/0 derivation: " + time + "ms");
private void signTransactions() throws Exception {
long time = System.currentTimeMillis();
cmdSet.select().checkOK();
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN("000000").checkOK();
deriveKeyFromParent = System.currentTimeMillis();
cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT).checkOK();
deriveKeyFromParent = System.currentTimeMillis() - deriveKeyFromParent;
cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes()).checkOK();
signTime = System.currentTimeMillis() - time;
}
private KeyPairGenerator keypairGenerator() throws Exception {
@@ -120,73 +129,4 @@ public class PerfTest {
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) {}
}
}
}
}
@@ -0,0 +1,54 @@
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 static Secrets testSecrets() throws NoSuchAlgorithmException, InvalidKeySpecException {
String pairingPassword = "WalletAppletTest";
byte[] pairingToken = Crypto.generatePairingKey(pairingPassword.toCharArray());
return new Secrets("000000", "123456789012", pairingPassword, pairingToken);
}
public String getPin() {
return pin;
}
public String getPuk() {
return puk;
}
public String getPairingPassword() {
return pairingPassword;
}
public byte[] getPairingToken() {
return pairingToken;
}
}
@@ -1,22 +1,24 @@
package im.status.applet_installer_test.appletinstaller;
import android.provider.Settings;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import java.io.IOException;
public class SecureChannel implements Channel {
private Channel channel;
public class SecureChannel {
private CardChannel channel;
private APDUWrapper wrapper;
public SecureChannel(Channel channel, Keys keys) {
public SecureChannel(CardChannel channel, Keys keys) {
this.channel = channel;
this.wrapper = new APDUWrapper(keys.getMacKeyData());
}
public APDUResponse send(APDUCommand cmd) throws IOException {
Logger.log(String.format("WRAPPING %s %n", HexUtils.byteArrayToHexString(cmd.serialize())));
Logger.d(String.format("WRAPPING %s %n", HexUtils.byteArrayToHexString(cmd.serialize())), false);
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
Logger.log(String.format("WRAPPED %s %n", HexUtils.byteArrayToHexString(wrappedCommand.serialize())));
Logger.d(String.format("WRAPPED %s %n", HexUtils.byteArrayToHexString(wrappedCommand.serialize())), false);
return this.channel.send(wrappedCommand);
}
}
@@ -0,0 +1,26 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.hardwallet_lite_android.io.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,8 +1,8 @@
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;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUResponse;
public class ExternalAuthenticate {
public static int CLA = 0x84;
@@ -1,15 +1,11 @@
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;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
public class InitializeUpdate {
public static final int CLA = 0x80;
@@ -32,11 +28,7 @@ public class InitializeUpdate {
}
public static byte[] generateChallenge() {
SecureRandom random = new SecureRandom();
byte challenge[] = new byte[8];
random.nextBytes(challenge);
return challenge;
return Crypto.randomBytes(8);
}
public Session verifyResponse(Keys cardKeys, APDUResponse resp) throws APDUException {
@@ -1,10 +1,10 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.hardwallet_lite_android.io.APDUCommand;
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;
@@ -44,7 +44,7 @@ public class InstallForInstall {
data.write(fullParams.length);
data.write(fullParams);
// empty install token
// empty perform token
data.write(0x00);
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray() );
@@ -1,10 +1,10 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.hardwallet_lite_android.io.APDUCommand;
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;
@@ -1,8 +1,8 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayInputStream;
import im.status.hardwallet_lite_android.io.APDUCommand;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -11,9 +11,6 @@ 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;
@@ -1,444 +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) {
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();
open = false;
} 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 {
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;
}
/**
* 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,6 +1,6 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUCommand;
public class Select {
private static final int CLA = 0x00;
@@ -1,6 +1,6 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUCommand;
public class Status {
private static final int CLA = 0x80;
@@ -1,440 +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.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;
/**
* 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_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_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 {
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 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 pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(String pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, 0, 0, pin.getBytes());
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 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);
}
}
+25 -22
View File
@@ -7,34 +7,37 @@
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
<ScrollView
android:id="@+id/textViewScroll"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="\n"
android:scrollbars="vertical"
android:padding="10dp"
android:gravity="bottom" />
android:layout_weight="1">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="30dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:enabled="true"
android:textIsSelectable="true" />
</ScrollView>
<Button
android:id="@+id/buttonInstall"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="install"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toStartOf="@+id/buttonPerfTest"
android:layout_marginEnd="8dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/textView" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp" app:layout_constraintHorizontal_bias="0.292"
app:layout_constraintVertical_bias="0.512"/>
android:text="Install Secure"/>
<Button
android:text="PerfTest"
android:layout_width="wrap_content"
android:id="@+id/buttonInstallTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/buttonPerfTest" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="52dp"
android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintVertical_bias="0.512"/>
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" />
</LinearLayout>
Binary file not shown.
@@ -1,6 +1,8 @@
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;
@@ -50,4 +52,12 @@ public class CryptoTest {
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));
}
}
@@ -0,0 +1,21 @@
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));
}
}
@@ -4,9 +4,6 @@ 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;
@@ -4,8 +4,6 @@ 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.*;
@@ -4,7 +4,6 @@ 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.*;
@@ -3,51 +3,43 @@ 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());
//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);
}
//ArrayList<APDUCommand> commands = new ArrayList<APDUCommand>();
//APDUCommand cmd;
//while((cmd = load.getCommand()) != null) {
// commands.add(cmd);
//}
assertEquals(31, commands.size());
//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 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));
//// 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));
//// Last command
//cmd = commands.get(30);
//assertEquals(0x80, cmd.getP1());
//assertEquals(30, cmd.getP2());
//expectedData = "080707080C08090A04050A0A06070706081A07085029031512201209190C0B0B0503060D09030E0B0C0C080A0A0603070706104622070914240A1611130D080B0F0A0D10110905040B2E271205030E0D0D0D0D0807140808030E1E10050321032C2A070606080D140C2723180B081D0A0707060811030D0407070608201B07091408252C2E39";
//data = cmd.getData();
//assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
}
}
@@ -4,7 +4,6 @@ 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.*;
@@ -4,8 +4,6 @@ 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.*;
+1
View File
@@ -19,6 +19,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB