Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3eb43b8e5 | ||
|
|
e2afb066c3 | ||
|
|
0272fadb85 | ||
|
|
cb8c5c8e2b | ||
|
|
00057359ce | ||
|
|
ea170172b5 | ||
|
|
2fdb108303 | ||
|
|
c7fc137724 | ||
|
|
97773b6c37 | ||
|
|
05780c1f9c | ||
|
|
37b7b9ebc8 | ||
|
|
9c1adc9507 | ||
|
|
99c277021c | ||
|
|
6b493118c5 | ||
|
|
535630f2ae | ||
|
|
7088d3acbf | ||
|
|
9470bc3314 | ||
|
|
68fd877214 | ||
|
|
cf41d9d7ff | ||
|
|
cdaf79eebe | ||
|
|
746ac5c8f0 | ||
|
|
93162a4c1e | ||
|
|
f1f5e8bd99 | ||
|
|
969c33aa20 | ||
|
|
c86ca40480 | ||
|
|
0eb18e79f1 | ||
|
|
6eb571157f | ||
|
|
27fe6df4d0 | ||
|
|
703c8ea860 | ||
|
|
79c3834859 | ||
|
|
3bc62e8e49 | ||
|
|
4309ef5792 | ||
|
|
61dbec1a7f | ||
|
|
75ec4d0723 | ||
|
|
8678b76612 | ||
|
|
ee5c3709c6 | ||
|
|
74b0ea6a07 | ||
|
|
d56d2df760 | ||
|
|
8480ff4173 |
Generated
+11
-2
@@ -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>
|
||||
|
||||
Generated
-12
@@ -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>
|
||||
@@ -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)
|
||||
|
||||

|
||||
+2
-2
@@ -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 {
|
||||
|
||||
Binary file not shown.
@@ -7,6 +7,8 @@ public class APDUResponse {
|
||||
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;
|
||||
@@ -39,7 +41,7 @@ public class APDUResponse {
|
||||
|
||||
public APDUResponse checkOK() throws APDUException {
|
||||
if (!isOK()) {
|
||||
throw new APDUException("Unexpected error SW");
|
||||
throw new APDUException(this.getSw(), "Unexpected error SW");
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
@@ -15,9 +15,9 @@ public class CardChannel implements Channel {
|
||||
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
byte[] apdu = cmd.serialize();
|
||||
Logger.log(String.format("COMMAND %s %n", HexUtils.byteArrayToHexString(apdu)));
|
||||
Logger.d(String.format("COMMAND %s", HexUtils.byteArrayToHexString(apdu)));
|
||||
byte[] resp = this.isoDep.transceive(apdu);
|
||||
Logger.log(String.format("RESPONSE %s %n", HexUtils.byteArrayToHexString(resp)));
|
||||
Logger.d(String.format("RESPONSE %s %n-----------------------", HexUtils.byteArrayToHexString(resp)));
|
||||
return new APDUResponse(resp);
|
||||
}
|
||||
}
|
||||
|
||||
+132
-12
@@ -1,29 +1,149 @@
|
||||
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 {
|
||||
private Tag tag;
|
||||
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_INSTALL_TEST = 2;
|
||||
public final static int ACTION_PERFTEST = 3;
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private AssetManager assets;
|
||||
private String capPath;
|
||||
private IsoDep isoDep;
|
||||
private int requestedAction;
|
||||
private long cardConnectedAt;
|
||||
private boolean running;
|
||||
|
||||
public CardManager(Tag tag) {
|
||||
this.tag = tag;
|
||||
public CardManager(NfcAdapter nfcAdapter, AssetManager assets, String capPath) {
|
||||
this.nfcAdapter = nfcAdapter;
|
||||
this.assets = assets;
|
||||
this.capPath = capPath;
|
||||
this.requestedAction = ACTION_NONE;
|
||||
}
|
||||
|
||||
public void connect() throws IOException {
|
||||
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_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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagDiscovered(Tag tag) {
|
||||
this.isoDep = IsoDep.get(tag);
|
||||
this.isoDep.setTimeout(120000);
|
||||
Logger.log("Is connected: " + this.isoDep.isConnected());
|
||||
this.isoDep.connect();
|
||||
try {
|
||||
this.isoDep = IsoDep.get(tag);
|
||||
this.isoDep.connect();
|
||||
this.isoDep.setTimeout(120000);
|
||||
} catch (IOException e) {
|
||||
Logger.e("error connecting to tag");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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, 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.running = false;
|
||||
this.requestedAction = ACTION_NONE;
|
||||
this.cardConnectedAt = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,28 @@ 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);
|
||||
|
||||
@@ -125,4 +134,33 @@ 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) {
|
||||
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
String token = encoder.encodeToString(randomBytes(length));
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,43 @@ 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.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
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.*;
|
||||
|
||||
public class Installer {
|
||||
private Channel plainChannel;
|
||||
private Channel 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) {
|
||||
public Installer(Channel channel, AssetManager assets, String capPath, boolean testSecrets) {
|
||||
this.plainChannel = channel;
|
||||
this.channel = 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);
|
||||
@@ -55,36 +58,71 @@ 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() + "/37", loadCmd);
|
||||
}
|
||||
Logger.log("---- After");
|
||||
|
||||
InstallForInstall installNDEF = new InstallForInstall(packageAID, ndefAppletAID, ndefInstanceAID, new byte[0]);
|
||||
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());
|
||||
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 = testSecrets ? Secrets.testSecrets() : Secrets.generate();
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+45
-88
@@ -4,68 +4,68 @@ 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 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 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.cardManager = new CardManager(nfcAdapter, assets, "wallet.cap");
|
||||
this.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(CardManager.ACTION_INSTALL);
|
||||
}
|
||||
});
|
||||
buttonInstallTest = (Button) findViewById(R.id.buttonInstallTest);
|
||||
buttonInstallTest.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
requestAction(CardManager.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(CardManager.ACTION_PERFTEST);
|
||||
}
|
||||
});
|
||||
|
||||
//Logger.setUILevel(Log.INFO);
|
||||
//Logger.setLevel(Log.INFO);
|
||||
}
|
||||
|
||||
private void logException(Exception e) {
|
||||
@@ -74,35 +74,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.cardManager != null) {
|
||||
clearTextView();
|
||||
this.cardManager.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 +103,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,7 +1,7 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.IsoDep;
|
||||
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;
|
||||
@@ -26,73 +26,109 @@ 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;
|
||||
|
||||
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_WHISPER = 0x01;
|
||||
static final byte EXPORT_KEY_P1_DATABASE = 0x02;
|
||||
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 = 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 static final byte[] SHARED_SECRET;
|
||||
|
||||
|
||||
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);
|
||||
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
|
||||
loadKeys();
|
||||
measureLogin();
|
||||
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 measureLogin() throws Exception {
|
||||
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();
|
||||
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}, 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 {
|
||||
@@ -104,13 +140,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, DERIVE_P1_SOURCE_CURRENT, false, false).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();
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -14,9 +14,9 @@ public class SecureChannel implements Channel {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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
-5
@@ -32,11 +32,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
-1
@@ -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() );
|
||||
|
||||
+34
-4
@@ -57,6 +57,12 @@ public class SecureChannelSession {
|
||||
* @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");
|
||||
@@ -74,9 +80,7 @@ public class SecureChannelSession {
|
||||
|
||||
keyAgreement.doPhase(cardKey, true);
|
||||
secret = keyAgreement.generateSecret();
|
||||
|
||||
open = false;
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
}
|
||||
@@ -299,7 +303,7 @@ public class SecureChannelSession {
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void unpairOthers(CardChannel apduChannel) throws IOException {
|
||||
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]);
|
||||
@@ -416,6 +420,32 @@ public class SecureChannelSession {
|
||||
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.
|
||||
|
||||
+62
-3
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -10,6 +11,7 @@ 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
|
||||
@@ -17,6 +19,7 @@ import java.security.PrivateKey;
|
||||
* 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;
|
||||
@@ -24,6 +27,7 @@ public class WalletAppletCommandSet {
|
||||
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;
|
||||
@@ -143,7 +147,7 @@ public class WalletAppletCommandSet {
|
||||
/**
|
||||
* Unpair all other clients.
|
||||
*/
|
||||
public void unpairOthers() throws IOException {
|
||||
public void unpairOthers() throws IOException, APDUException {
|
||||
secureChannel.unpairOthers(apduChannel);
|
||||
}
|
||||
|
||||
@@ -173,6 +177,19 @@ public class WalletAppletCommandSet {
|
||||
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.
|
||||
@@ -190,12 +207,26 @@ public class WalletAppletCommandSet {
|
||||
* 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(String pin) throws IOException {
|
||||
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, 0, 0, pin.getBytes());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -361,6 +392,17 @@ public class WalletAppletCommandSet {
|
||||
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
|
||||
@@ -437,4 +479,21 @@ public class WalletAppletCommandSet {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+21
@@ -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));
|
||||
}
|
||||
}
|
||||
+29
-30
@@ -12,42 +12,41 @@ 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));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Reference in New Issue
Block a user