Compare commits
63
Commits
card-watcher
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2e7f698ac | ||
|
|
0801636e78 | ||
|
|
5975f1d02d | ||
|
|
83b418d73f | ||
|
|
086cc77f67 | ||
|
|
5ea1a2397b | ||
|
|
7511d3ea6c | ||
|
|
a9982c5fdc | ||
|
|
73192b3ba0 | ||
|
|
a22528dc0f | ||
|
|
ffd5b1a83f | ||
|
|
dc2171f1dd | ||
|
|
c91e4d7848 | ||
|
|
c8f13db627 | ||
|
|
98caa6106e | ||
|
|
85177fe5d3 | ||
|
|
0045a2259a | ||
|
|
2e6a10fe81 | ||
|
|
e708ef9419 | ||
|
|
0cd03ca32f | ||
|
|
084baa5501 | ||
|
|
3f8a6c94dc | ||
|
|
341c5cbe1d | ||
|
|
a13b70d2ac | ||
|
|
60faf16d61 | ||
|
|
77d5774d22 | ||
|
|
d27f55fb59 | ||
|
|
8d442959d3 | ||
|
|
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 |
+2
-11
@@ -1,6 +1,7 @@
|
||||
/local.properties
|
||||
/.idea/modules.xml
|
||||
/.idea
|
||||
/build
|
||||
*.iml
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
@@ -36,16 +37,6 @@ proguard/
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
.idea/caches
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following line if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
|
||||
Generated
-29
@@ -1,29 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<Objective-C-extensions>
|
||||
<file>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
||||
</file>
|
||||
<class>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
||||
</class>
|
||||
<extensions>
|
||||
<pair source="cpp" header="h" fileNamingConvention="NONE" />
|
||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
-34
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="5">
|
||||
<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" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<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" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-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>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,3 @@
|
||||
# Keycard Installer for Android
|
||||
|
||||
discountinued - use [https://github.com/status-im/keycard-cli](https://github.com/status-im/keycard-cli)
|
||||
+6
-5
@@ -3,11 +3,11 @@ apply plugin: 'com.android.application'
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
defaultConfig {
|
||||
applicationId "im.status.applet_installer_test.appletinstaller"
|
||||
applicationId "im.status.keycard_installer.installer"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 220
|
||||
versionName "2.2.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
@@ -22,8 +22,9 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
|
||||
implementation 'com.github.status-im.status-keycard-java:android:7ce0136'
|
||||
implementation 'de.cotech:nfc-sweetspot:1.1'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("im.status.applet_installer_test.appletinstaller", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.status.applet_installer_test.appletinstaller">
|
||||
package="im.status.keycard.installer">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-feature android:name="android.hardware.nfc.hce"
|
||||
@@ -13,13 +13,18 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity">
|
||||
<activity android:name="im.status.keycard.installer.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="de.cotech.sweetspot.ShowNfcSweetspotActivity"
|
||||
android:exported="false"
|
||||
/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,67 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class APDUCommand {
|
||||
protected int cla;
|
||||
protected int ins;
|
||||
protected int p1;
|
||||
protected int p2;
|
||||
protected int lc;
|
||||
protected byte[] data;
|
||||
protected boolean needsLE;
|
||||
|
||||
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||
this(cla, ins, p1, p2, data, false);
|
||||
}
|
||||
|
||||
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
|
||||
this.cla = cla;
|
||||
this.ins = ins;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.data = data;
|
||||
this.needsLE = needsLE;
|
||||
}
|
||||
|
||||
public byte[] serialize() throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(this.cla);
|
||||
out.write(this.ins);
|
||||
out.write(this.p1);
|
||||
out.write(this.p2);
|
||||
out.write(this.data.length);
|
||||
out.write(this.data);
|
||||
|
||||
if (this.needsLE) {
|
||||
out.write(0); // Response length
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public int getCla() {
|
||||
return cla;
|
||||
}
|
||||
|
||||
public int getIns() {
|
||||
return ins;
|
||||
}
|
||||
|
||||
public int getP1() {
|
||||
return p1;
|
||||
}
|
||||
|
||||
public int getP2() {
|
||||
return p2;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public boolean getNeedsLE() {
|
||||
return this.needsLE;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
public class APDUException extends Exception {
|
||||
public final int sw;
|
||||
|
||||
public APDUException(int sw, String message) {
|
||||
super(message + ", 0x" + String.format("%04X", sw));
|
||||
this.sw = sw;
|
||||
}
|
||||
|
||||
public APDUException(String message) {
|
||||
super(message);
|
||||
this.sw = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,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(this.getSw(), "Unexpected error SW");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public int getSw() {
|
||||
return this.sw;
|
||||
}
|
||||
|
||||
public int getSw1() {
|
||||
return this.sw1;
|
||||
}
|
||||
|
||||
public int getSw2() {
|
||||
return this.sw2;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return this.apdu;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
public class APDUWrapper {
|
||||
private byte[] macKeyData;
|
||||
private byte[] icv;
|
||||
|
||||
public APDUWrapper(byte[] macKeyData) {
|
||||
this.macKeyData = macKeyData;
|
||||
this.icv = Crypto.NullBytes8.clone();
|
||||
}
|
||||
|
||||
public APDUCommand wrap(APDUCommand cmd) {
|
||||
try {
|
||||
int cla = (cmd.getCla() | 0x04) & 0xff;
|
||||
byte[] data = cmd.getData();
|
||||
|
||||
ByteArrayOutputStream macData = new ByteArrayOutputStream();
|
||||
macData.write(cla);
|
||||
macData.write(cmd.getIns());
|
||||
macData.write(cmd.getP1());
|
||||
macData.write(cmd.getP2());
|
||||
macData.write(data.length + 8);
|
||||
macData.write(data);
|
||||
|
||||
byte[] icv;
|
||||
if (Arrays.equals(this.icv, Crypto.NullBytes8)) {
|
||||
icv = this.icv;
|
||||
} else {
|
||||
icv = Crypto.encryptICV(this.macKeyData, this.icv);
|
||||
}
|
||||
|
||||
byte[] mac = Crypto.macFull3des(this.macKeyData, Crypto.appendDESPadding(macData.toByteArray()), icv);
|
||||
byte[] newData = new byte[data.length + mac.length];
|
||||
System.arraycopy(data, 0, newData, 0, data.length );
|
||||
System.arraycopy(mac, 0, newData, data.length, mac.length );
|
||||
|
||||
APDUCommand wrapped = new APDUCommand(cla, cmd.getIns(), cmd.getP1(), cmd.getP2(), newData, cmd.getNeedsLE());
|
||||
this.icv = mac.clone();
|
||||
|
||||
return wrapped;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("error wrapping APDU command.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getICV() {
|
||||
return this.icv;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.nfc.tech.IsoDep;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.CardManager;
|
||||
|
||||
public class CardChannel implements Channel {
|
||||
private IsoDep isoDep;
|
||||
|
||||
public CardChannel(IsoDep isoDep) {
|
||||
this.isoDep = isoDep;
|
||||
}
|
||||
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
byte[] apdu = cmd.serialize();
|
||||
Logger.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;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
|
||||
public class Crypto {
|
||||
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
public static byte[] deriveKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
|
||||
byte[] key24 = resizeKey24(cardKey);
|
||||
|
||||
try {
|
||||
byte[] derivationData = new byte[16];
|
||||
// 2 bytes constant
|
||||
System.arraycopy(purposeData, 0, derivationData, 0, 2);
|
||||
// 2 bytes sequence counter + 12 bytes 0x00
|
||||
System.arraycopy(seq, 0, derivationData, 2, 2);
|
||||
|
||||
SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8));
|
||||
|
||||
return cipher.doFinal(derivationData);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException("error generating session keys.", e);
|
||||
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
|
||||
throw new RuntimeException("error generating session keys.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] appendDESPadding(byte[] data) {
|
||||
int length = data.length + 1;
|
||||
for (; length % 8 != 0; length++){
|
||||
}
|
||||
byte[] newData = new byte[length];
|
||||
System.arraycopy(data, 0, newData, 0, data.length);
|
||||
newData[data.length] = (byte)0x80;
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
public static boolean verifyCryptogram(byte[] key, byte[] hostChallenge, byte[] cardChallenge, byte[] cardCryptogram) {
|
||||
byte[] data = new byte[hostChallenge.length + cardChallenge.length];
|
||||
System.arraycopy(hostChallenge, 0, data, 0, hostChallenge.length);
|
||||
System.arraycopy(cardChallenge, 0, data, hostChallenge.length, cardChallenge.length);
|
||||
byte[] paddedData = appendDESPadding(data);
|
||||
byte[] calculated = mac3des(key, paddedData, NullBytes8);
|
||||
|
||||
return Arrays.equals(calculated , cardCryptogram);
|
||||
}
|
||||
|
||||
public static byte[] mac3des(byte[] keyData, byte[] data, byte[] iv) {
|
||||
try {
|
||||
SecretKeySpec key = new SecretKeySpec(resizeKey24(keyData), "DESede");
|
||||
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
|
||||
byte[] result = cipher.doFinal(data, 0, 24);
|
||||
byte[] tail = new byte[8];
|
||||
System.arraycopy(result, 16, tail, 0, 8);
|
||||
return tail;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("error calculating mac.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] macFull3des(byte[] keyData, byte[] data, byte[] iv) {
|
||||
try {
|
||||
SecretKeySpec keyDes = new SecretKeySpec(resizeKey8(keyData), "DES");
|
||||
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding");
|
||||
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
|
||||
|
||||
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
|
||||
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding");
|
||||
byte[] des3Iv = iv.clone();
|
||||
|
||||
if (data.length > 8) {
|
||||
byte[] tmp = cipherDes.doFinal(data, 0, data.length - 8);
|
||||
System.arraycopy(tmp, tmp.length - 8, des3Iv, 0, 8);
|
||||
}
|
||||
|
||||
cipherDes3.init(Cipher.ENCRYPT_MODE, keyDes3, new IvParameterSpec(des3Iv));
|
||||
byte[] result = cipherDes3.doFinal(data, data.length - 8, 8);
|
||||
byte[] tail = new byte[8];
|
||||
System.arraycopy(result, result.length - 8, tail, 0, 8);
|
||||
return tail;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("error generating full triple DES MAC.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] resizeKey24(byte[] keyData) {
|
||||
byte[] key = new byte[24];
|
||||
System.arraycopy(keyData, 0, key, 0, 16);
|
||||
System.arraycopy(keyData, 0, key, 16, 8);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public static byte[] resizeKey8(byte[] keyData) {
|
||||
byte[] key = new byte[8];
|
||||
System.arraycopy(keyData, 0, key, 0, 8);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
|
||||
SecretKeySpec key = new SecretKeySpec(resizeKey8(macKeyData), "DES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
return cipher.doFinal(mac);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("error generating ICV.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
|
||||
public class HexUtils {
|
||||
public static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len/2];
|
||||
|
||||
for(int i = 0; i < len; i+=2){
|
||||
data[i/2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
|
||||
|
||||
public static String byteArrayToHexString(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
int v;
|
||||
|
||||
for(int j=0; j < bytes.length; j++) {
|
||||
v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v>>>4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
|
||||
return new String(hexChars);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
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;
|
||||
|
||||
public class Installer {
|
||||
private Channel channel;
|
||||
private Keys cardKeys;
|
||||
private AssetManager assets;
|
||||
private String capPath;
|
||||
|
||||
static final byte[] cardKeyData = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
|
||||
|
||||
public Installer(Channel channel, AssetManager assets, String capPath) {
|
||||
this.channel = channel;
|
||||
this.cardKeys = new Keys(cardKeyData, cardKeyData);
|
||||
this.assets = assets;
|
||||
this.capPath = capPath;
|
||||
}
|
||||
|
||||
public void start() throws IOException, APDUException {
|
||||
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));
|
||||
|
||||
byte[] hostChallenge = InitializeUpdate.generateChallenge();
|
||||
InitializeUpdate init = new InitializeUpdate(hostChallenge);
|
||||
resp = this.send("init update", init.getCommand());
|
||||
|
||||
Session session = init.verifyResponse(this.cardKeys, resp);
|
||||
Keys sessionKeys = session.getKeys();
|
||||
|
||||
this.channel = new SecureChannel(this.channel, sessionKeys);
|
||||
|
||||
ExternalAuthenticate auth = new ExternalAuthenticate(sessionKeys.getEncKeyData(), session.getCardChallenge(), hostChallenge);
|
||||
resp = this.send("external auth", auth.getCommand());
|
||||
if (!auth.checkResponse(resp)) {
|
||||
throw new APDUException(resp.getSw(), "bad external authenticate response");
|
||||
}
|
||||
|
||||
//Status status = new Status(Status.P1_EXECUTABLE_LOAD_FILES_AND_MODULES);
|
||||
//resp = this.send("status", status.getCommand());
|
||||
|
||||
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
|
||||
InstallForLoad preLoad = new InstallForLoad(aid, sdaid);
|
||||
this.send("install 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);
|
||||
}
|
||||
Logger.log("---- After");
|
||||
|
||||
|
||||
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
|
||||
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
|
||||
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
|
||||
|
||||
byte[] params = HexUtils.hexStringToByteArray("3236393732333032383339318bfb5c8ea8b78a84b9efbfbc897d80312e71e559145947f447d8b6d0d9fcdb55");
|
||||
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params);
|
||||
this.send("install and make selectable", install.getCommand());
|
||||
}
|
||||
|
||||
private APDUResponse send(String description, APDUCommand cmd) throws IOException, APDUException {
|
||||
Logger.log("sending command " + description);
|
||||
APDUResponse resp = this.channel.send(cmd);
|
||||
if (!resp.isOK()) {
|
||||
throw new APDUException(resp.getSw(), "bad response for command " + description);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
private byte[] getSDAID(byte[] data) throws IOException, APDUException {
|
||||
Tlv tlv = new Tlv(data);
|
||||
tlv = tlv.find((byte) 0x6F);
|
||||
if (tlv == null) {
|
||||
throw new APDUException("error searching for tag 0x6F in discover response");
|
||||
}
|
||||
|
||||
tlv = tlv.find((byte) 0x84);
|
||||
if (tlv == null) {
|
||||
throw new APDUException("error searching for tag 0x84 in discover response");
|
||||
}
|
||||
|
||||
return tlv.getValue();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
public class Keys {
|
||||
public byte[] encKeyData;
|
||||
public byte[] macKeyData;
|
||||
|
||||
public Keys(byte[] encKeyData, byte[] macKeyData) {
|
||||
this.encKeyData = encKeyData;
|
||||
this.macKeyData = macKeyData;
|
||||
}
|
||||
|
||||
public byte[] getEncKeyData() {
|
||||
return encKeyData;
|
||||
}
|
||||
|
||||
public byte[] getMacKeyData() {
|
||||
return macKeyData;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
interface LogListener {
|
||||
public void log(String m);
|
||||
}
|
||||
|
||||
public class Logger {
|
||||
private static LogListener listener;
|
||||
|
||||
public static void setListener(LogListener l) {
|
||||
listener = l;
|
||||
}
|
||||
|
||||
public static void log(String m) {
|
||||
if (m != null) {
|
||||
Log.d("installer-debug", m);
|
||||
if (listener != null) {
|
||||
listener.log(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void log(byte[] m) {
|
||||
log(HexUtils.byteArrayToHexString(m));
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
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.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 {
|
||||
static {
|
||||
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
|
||||
}
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private TextView textView;
|
||||
private Button buttonInstall;
|
||||
private Button buttonPerfTest;
|
||||
private Tag tag;
|
||||
private boolean installationAttempted;
|
||||
private TagManager tagManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
Logger.setListener(this);
|
||||
this.tagManager = new TagManager(nfcAdapter);
|
||||
this.tagManager.start();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void logException(Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) {
|
||||
msg = "exception without message";
|
||||
}
|
||||
|
||||
Logger.log("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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (nfcAdapter != null) {
|
||||
nfcAdapter.enableReaderMode(this, this.tagManager,
|
||||
NfcAdapter.FLAG_READER_NFC_A |
|
||||
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (nfcAdapter != null) {
|
||||
nfcAdapter.disableReaderMode(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagDiscovered(Tag tag) {
|
||||
Logger.log("tag found");
|
||||
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;
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
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 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 WalletAppletCommandSet cmdSet;
|
||||
private SecureChannelSession secureChannel;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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 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.autoOpenSecureChannel();
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.unpairOthers(); // Recover in case of non-clean termination
|
||||
loadKeys();
|
||||
measureLogin();
|
||||
cmdSet.select();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.autoUnpair();
|
||||
}
|
||||
|
||||
private void measureLogin() 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");
|
||||
}
|
||||
|
||||
private void loadKeys() throws Exception {
|
||||
KeyPairGenerator g = keypairGenerator();
|
||||
KeyPair keyPair = g.generateKeyPair();
|
||||
byte[] chainCode = new byte[32];
|
||||
new Random().nextBytes(chainCode);
|
||||
|
||||
cmdSet.loadKey(keyPair, false, chainCode).checkOK();
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
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;
|
||||
|
||||
Logger.log("Total time for m/44'/60'/0'/0/0 derivation: " + time + "ms");
|
||||
}
|
||||
|
||||
private KeyPairGenerator keypairGenerator() throws Exception {
|
||||
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
||||
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
|
||||
g.initialize(ecSpec);
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
private byte[] extractPublicKeyFromSelect(byte[] select) {
|
||||
return Arrays.copyOfRange(select, 22, 22 + select[21]);
|
||||
}
|
||||
|
||||
private byte[] derivePublicKey(byte[] data) throws Exception {
|
||||
byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]);
|
||||
byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length);
|
||||
byte[] hash = MessageDigest.getInstance("SHA256").digest("STATUS KEY DERIVATION".getBytes());
|
||||
|
||||
pubKey[0] = 0x02;
|
||||
|
||||
ECPoint candidate = CURVE.getCurve().decodePoint(pubKey);
|
||||
if (!verifySig(hash, signature, candidate)) {
|
||||
pubKey[0] = 0x03;
|
||||
candidate = CURVE.getCurve().decodePoint(pubKey);
|
||||
if (!verifySig(hash, signature, candidate)) {
|
||||
throw new Exception("Public key is incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
return candidate.getEncoded(false);
|
||||
}
|
||||
|
||||
private boolean verifySig(byte[] hash, byte[] signature, ECPoint pub) {
|
||||
ECDSASigner signer = new ECDSASigner();
|
||||
ECPublicKeyParameters params = new ECPublicKeyParameters(pub, CURVE);
|
||||
signer.init(false, params);
|
||||
ECDSASignature sig = ECDSASignature.decodeFromDER(signature);
|
||||
return signer.verifySignature(hash, sig.r, sig.s);
|
||||
}
|
||||
|
||||
static class ECDSASignature {
|
||||
/** The two components of the signature. */
|
||||
public final BigInteger r, s;
|
||||
|
||||
/**
|
||||
* Constructs a signature with the given components. Does NOT automatically canonicalise the signature.
|
||||
*/
|
||||
public ECDSASignature(BigInteger r, BigInteger s) {
|
||||
this.r = r;
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
public static ECDSASignature decodeFromDER(byte[] bytes) {
|
||||
ASN1InputStream decoder = null;
|
||||
try {
|
||||
decoder = new ASN1InputStream(bytes);
|
||||
DLSequence seq = (DLSequence) decoder.readObject();
|
||||
if (seq == null)
|
||||
throw new RuntimeException("Reached past end of ASN.1 stream.");
|
||||
ASN1Integer r, s;
|
||||
try {
|
||||
r = (ASN1Integer) seq.getObjectAt(0);
|
||||
s = (ASN1Integer) seq.getObjectAt(1);
|
||||
} catch (ClassCastException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
// OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
|
||||
// Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
|
||||
return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (decoder != null)
|
||||
try { decoder.close(); } catch (IOException x) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.provider.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SecureChannel implements Channel {
|
||||
private Channel channel;
|
||||
private APDUWrapper wrapper;
|
||||
|
||||
public SecureChannel(Channel channel, Keys keys) {
|
||||
this.channel = channel;
|
||||
this.wrapper = new APDUWrapper(keys.getMacKeyData());
|
||||
}
|
||||
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
Logger.log(String.format("WRAPPING %s %n", HexUtils.byteArrayToHexString(cmd.serialize())));
|
||||
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
|
||||
Logger.log(String.format("WRAPPED %s %n", HexUtils.byteArrayToHexString(wrappedCommand.serialize())));
|
||||
return this.channel.send(wrappedCommand);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
public class Session {
|
||||
private Keys keys;
|
||||
private byte[] cardChallenge;
|
||||
|
||||
public Session(Keys keys, byte[] cardChallenge) {
|
||||
this.keys = keys;
|
||||
this.cardChallenge = cardChallenge;
|
||||
}
|
||||
|
||||
public Keys getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public byte[] getCardChallenge() {
|
||||
return cardChallenge;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class TagManager extends Thread implements NfcAdapter.ReaderCallback {
|
||||
NfcAdapter nfcAdapter;
|
||||
IsoDep isoDep;
|
||||
|
||||
public TagManager(NfcAdapter nfcAdapter) {
|
||||
this.nfcAdapter = nfcAdapter;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.isoDep != null && this.isoDep.isConnected();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
boolean connected = this.isConnected();
|
||||
|
||||
while(true) {
|
||||
boolean newConnected = this.isConnected();
|
||||
if (newConnected != connected) {
|
||||
connected = newConnected;
|
||||
Logger.log("tag " + (connected ? "connected" : "disconnected"));
|
||||
|
||||
if (!newConnected) {
|
||||
this.isoDep = null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Logger.log("error in TagManager thread: " + e.getMessage());
|
||||
this.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagDiscovered(Tag tag) {
|
||||
this.isoDep = IsoDep.get(tag);
|
||||
try {
|
||||
this.isoDep.connect();
|
||||
} catch (IOException e) {
|
||||
Logger.log("error connecting to tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class Tlv {
|
||||
private byte tag;
|
||||
private byte[] value;
|
||||
|
||||
public Tlv(byte[] value) {
|
||||
this((byte) 0x00, value);
|
||||
}
|
||||
|
||||
public Tlv(byte tag, byte[] value) {
|
||||
this.tag = tag;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Tlv find(byte tag) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
while(offset + 2 < value.length) {
|
||||
byte childTag = this.value[offset];
|
||||
offset++;
|
||||
|
||||
int childLength = this.value[offset] & 0xff;
|
||||
offset++;
|
||||
|
||||
if (offset + childLength - 1 >= this.value.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = new byte[childLength];
|
||||
|
||||
System.arraycopy(this.value, offset, data, 0, childLength);
|
||||
offset += childLength;
|
||||
|
||||
if (childTag == tag) {
|
||||
return new Tlv(childTag, data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte getTag() {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUResponse;
|
||||
import im.status.applet_installer_test.appletinstaller.Crypto;
|
||||
|
||||
public class ExternalAuthenticate {
|
||||
public static int CLA = 0x84;
|
||||
public static int INS = 0x82;
|
||||
public static int P1 = 0x01;
|
||||
public static int P2 = 0x00;
|
||||
|
||||
private byte[] encKeyData;
|
||||
private byte[] cardChallenge;
|
||||
private byte[] hostChallenge;
|
||||
|
||||
public ExternalAuthenticate(byte[] encKeyData, byte[] cardChallenge, byte[] hostChallenge) {
|
||||
this.encKeyData = encKeyData;
|
||||
this.cardChallenge = cardChallenge;
|
||||
this.hostChallenge = hostChallenge;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() {
|
||||
return new APDUCommand(CLA, INS, P1, P2, this.getHostCryptogram());
|
||||
}
|
||||
|
||||
public byte[] getHostCryptogram() {
|
||||
byte[] data = new byte[this.cardChallenge.length + this.hostChallenge.length];
|
||||
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
|
||||
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
|
||||
byte[] paddedData = Crypto.appendDESPadding(data);
|
||||
|
||||
return Crypto.mac3des(this.encKeyData, paddedData, Crypto.NullBytes8);
|
||||
}
|
||||
|
||||
public boolean checkResponse(APDUResponse resp) {
|
||||
return resp.getSw() == APDUResponse.SW_OK;
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
public class GetData {
|
||||
public static final int CLA = 0x80;
|
||||
public static final int INS = 0x50;
|
||||
public static final int P1 = 0;
|
||||
public static final int P2 = 0;
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUException;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUResponse;
|
||||
import im.status.applet_installer_test.appletinstaller.Crypto;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
import im.status.applet_installer_test.appletinstaller.Keys;
|
||||
import im.status.applet_installer_test.appletinstaller.Logger;
|
||||
import im.status.applet_installer_test.appletinstaller.Session;
|
||||
|
||||
public class InitializeUpdate {
|
||||
public static final int CLA = 0x80;
|
||||
public static final int INS = 0x50;
|
||||
public static final int P1 = 0;
|
||||
public static final int P2 = 0;
|
||||
|
||||
public static byte[] DERIVATION_PURPOSE_ENC = new byte[]{(byte) 0x01, (byte) 0x82};
|
||||
public static byte[] DERIVATION_PURPOSE_MAC = new byte[]{(byte) 0x01, (byte) 0x01};
|
||||
public static byte[] DERIVATION_PURPOSE_DEK = new byte[]{(byte) 0x01, (byte) 0x81};
|
||||
|
||||
private byte[] hostChallenge;
|
||||
|
||||
public InitializeUpdate(byte[] challenge) {
|
||||
this.hostChallenge = challenge;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() {
|
||||
return new APDUCommand(CLA, INS, P1, P2, this.hostChallenge, true);
|
||||
}
|
||||
|
||||
public static byte[] generateChallenge() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte challenge[] = new byte[8];
|
||||
random.nextBytes(challenge);
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public Session verifyResponse(Keys cardKeys, APDUResponse resp) throws APDUException {
|
||||
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
|
||||
throw new APDUException(resp.getSw(), "security confition not satisfied");
|
||||
}
|
||||
|
||||
if (resp.getSw() == APDUResponse.SW_AUTHENTICATION_METHOD_BLOCKED) {
|
||||
throw new APDUException(resp.getSw(), "authentication method blocked");
|
||||
}
|
||||
|
||||
byte[] data = resp.getData();
|
||||
|
||||
if (data.length != 28) {
|
||||
throw new APDUException(resp.getSw(), String.format("bad data length, expected 28, got %d", data.length));
|
||||
}
|
||||
|
||||
byte[] cardChallenge = new byte[8];
|
||||
System.arraycopy(data, 12, cardChallenge, 0, 8);
|
||||
|
||||
byte[] cardCryptogram = new byte[8];
|
||||
System.arraycopy(data, 20, cardCryptogram, 0, 8);
|
||||
|
||||
byte[] seq = new byte[2];
|
||||
System.arraycopy(data, 12, seq, 0, 2);
|
||||
|
||||
byte[] sessionEncKey = Crypto.deriveKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
|
||||
byte[] sessionMacKey = Crypto.deriveKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
|
||||
|
||||
Keys sessionKeys = new Keys(sessionEncKey, sessionMacKey);
|
||||
|
||||
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), this.hostChallenge, cardChallenge, cardCryptogram);
|
||||
if (!verified) {
|
||||
throw new APDUException("error verifying card cryptogram.");
|
||||
}
|
||||
|
||||
return new Session(sessionKeys, cardChallenge);
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
|
||||
public class InstallForInstall {
|
||||
public static final int CLA = 0x80;
|
||||
public static final int INS = 0xE6;
|
||||
public static final int P1 = 0x0C;
|
||||
public static final int P2 = 0;
|
||||
|
||||
private byte[] packageAID;
|
||||
private byte[] appletAID;
|
||||
private byte[] instanceAID;
|
||||
private byte[] params;
|
||||
|
||||
public InstallForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) {
|
||||
this.packageAID = packageAID;
|
||||
this.appletAID = appletAID;
|
||||
this.instanceAID = instanceAID;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() throws IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
data.write(this.packageAID.length);
|
||||
data.write(this.packageAID);
|
||||
data.write(this.appletAID.length);
|
||||
data.write(this.appletAID);
|
||||
data.write(this.instanceAID.length);
|
||||
data.write(this.instanceAID);
|
||||
|
||||
byte[] privileges = new byte[]{0x00};
|
||||
data.write(privileges.length);
|
||||
data.write(privileges);
|
||||
|
||||
byte[] fullParams = new byte[2 + params.length];
|
||||
fullParams[0] = (byte) 0xC9;
|
||||
fullParams[1] = (byte) params.length;
|
||||
System.arraycopy(params, 0, fullParams, 2, params.length);
|
||||
|
||||
data.write(fullParams.length);
|
||||
data.write(fullParams);
|
||||
|
||||
// empty install token
|
||||
data.write(0x00);
|
||||
|
||||
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray() );
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
|
||||
public class InstallForLoad {
|
||||
public static final int CLA = 0x80;
|
||||
public static final int INS = 0xE6;
|
||||
public static final int P1 = 0x02;
|
||||
public static final int P2 = 0;
|
||||
|
||||
private byte[] aid;
|
||||
private byte[] sdaid;
|
||||
|
||||
public InstallForLoad(byte[] aid, byte[] sdaid) {
|
||||
this.aid = aid;
|
||||
this.sdaid = sdaid;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() throws IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
data.write(this.aid.length);
|
||||
data.write(this.aid);
|
||||
data.write(this.sdaid.length);
|
||||
data.write(this.sdaid);
|
||||
|
||||
// empty hash length and hash
|
||||
data.write(0x00);
|
||||
data.write(0x00);
|
||||
data.write(0x00);
|
||||
|
||||
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray());
|
||||
}
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
public class Load {
|
||||
public static final int CLA = 0x80;
|
||||
public static final int INS = 0xE8;
|
||||
|
||||
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
|
||||
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
|
||||
|
||||
private String path;
|
||||
private int offset;
|
||||
private int count;
|
||||
private byte[] fullData;
|
||||
|
||||
public Load(InputStream in) throws FileNotFoundException, IOException {
|
||||
this.path = path;
|
||||
this.offset = 0;
|
||||
this.count = 0;
|
||||
Map<String, byte[]> files = this.loadFiles(in);
|
||||
in.close();
|
||||
this.fullData = this.getCode(files);
|
||||
}
|
||||
|
||||
public Map<String, byte[]> loadFiles(InputStream in) throws IOException {
|
||||
Map<String, byte[]> files = new LinkedHashMap<>();
|
||||
ZipInputStream zip = new ZipInputStream(in);
|
||||
ZipEntry entry = zip.getNextEntry();
|
||||
|
||||
while(entry != null) {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[1024];
|
||||
int count;
|
||||
while ((count = zip.read(buf)) != -1) {
|
||||
data.write(buf, 0, count);
|
||||
}
|
||||
String name = baseName(entry.getName());
|
||||
files.put(name, data.toByteArray());
|
||||
entry = zip.getNextEntry();
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private String baseName(String path) {
|
||||
String[] parts = path.split("[/.]");
|
||||
return parts[parts.length - 2];
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() {
|
||||
int blockSize = 247; // 255 - 8 bytes for MAC
|
||||
if (this.offset >= this.fullData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int rangeEnd = this.offset + blockSize;
|
||||
if (rangeEnd >= this.fullData.length) {
|
||||
rangeEnd = this.fullData.length;
|
||||
}
|
||||
|
||||
int size = rangeEnd - offset;
|
||||
byte[] data = new byte[size];
|
||||
System.arraycopy(this.fullData, this.offset, data, 0, size);
|
||||
|
||||
boolean isLast = this.offset + size >= this.fullData.length;
|
||||
int p1 = isLast ? 0x80 : 0;
|
||||
APDUCommand cmd = new APDUCommand(CLA, INS, p1, this.count, data);
|
||||
|
||||
this.offset += size;
|
||||
this.count++;
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private byte[] encodeFullLength(int length) {
|
||||
if (length < 0x80) {
|
||||
return new byte[]{(byte) length};
|
||||
} else if (length < 0xFF) {
|
||||
return new byte[]{(byte) 0x81, (byte) length};
|
||||
} else if (length < 0xFFFF) {
|
||||
return new byte[]{
|
||||
(byte) 0x82,
|
||||
(byte) ((length & 0xFF00) >> 8),
|
||||
(byte) (length & 0xFF),
|
||||
};
|
||||
} else {
|
||||
return new byte[]{
|
||||
(byte) 0x83,
|
||||
(byte) ((length & 0xFF0000) >> 16),
|
||||
(byte) ((length & 0xFF00) >> 8),
|
||||
(byte) (length & 0xFF),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getCode(Map<String, byte[]> files) throws IOException {
|
||||
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
|
||||
|
||||
for (String name : fileNames) {
|
||||
byte[] fileData = files.get(name);
|
||||
if (fileData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dataStream.write(fileData);
|
||||
}
|
||||
|
||||
byte[] data = dataStream.toByteArray();
|
||||
byte[] encodedFullLength = encodeFullLength(data.length);
|
||||
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
|
||||
|
||||
fullData[0] = (byte) 0xC4;
|
||||
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
|
||||
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
|
||||
|
||||
return fullData;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
-444
@@ -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, APDUException {
|
||||
for (int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) {
|
||||
if (i != pairingIndex) {
|
||||
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
|
||||
transmit(apduChannel, openSecureChannel).checkOK();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext
|
||||
* already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array
|
||||
* the returned data will still contain the IV and padding.
|
||||
*
|
||||
* @param data the plaintext data
|
||||
* @return the encrypted data
|
||||
*/
|
||||
private byte[] encryptAPDU(byte[] data) {
|
||||
assert data.length <= PAYLOAD_MAX_SIZE;
|
||||
|
||||
try {
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
return sessionCipher.doFinal(data);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding
|
||||
* and can be potentially empty.
|
||||
*
|
||||
* @param data the ciphetext
|
||||
* @return the plaintext
|
||||
*/
|
||||
private byte[] decryptAPDU(byte[] data) {
|
||||
try {
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
return sessionCipher.doFinal(data);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a command APDU with MAC and encrypted data.
|
||||
*
|
||||
* @param cla the CLA byte
|
||||
* @param ins the INS byte
|
||||
* @param p1 the P1 byte
|
||||
* @param p2 the P2 byte
|
||||
* @param data the data, can be an empty array but not null
|
||||
* @return the command APDU
|
||||
*/
|
||||
public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||
byte[] finalData;
|
||||
|
||||
if (open) {
|
||||
data = encryptAPDU(data);
|
||||
byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
updateIV(meta, data);
|
||||
|
||||
finalData = Arrays.copyOf(iv, iv.length + data.length);
|
||||
System.arraycopy(data, 0, finalData, iv.length, data.length);
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
return new APDUCommand(cla, ins, p1, p2, finalData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits a protected command APDU and unwraps the response data. The MAC is verified, the data decrypted and the
|
||||
* SW read from the payload.
|
||||
*
|
||||
* @param apduChannel the APDU channel
|
||||
* @param apdu the APDU to send
|
||||
* @return the unwrapped response APDU
|
||||
* @throws IOException transmission error
|
||||
*/
|
||||
public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException {
|
||||
APDUResponse resp = apduChannel.send(apdu);
|
||||
|
||||
if (resp.getSw() == 0x6982) {
|
||||
open = false;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
byte[] data = resp.getData();
|
||||
byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
byte[] mac = Arrays.copyOf(data, iv.length);
|
||||
data = Arrays.copyOfRange(data, iv.length, data.length);
|
||||
|
||||
byte[] plainData = decryptAPDU(data);
|
||||
|
||||
updateIV(meta, data);
|
||||
|
||||
if (!Arrays.equals(iv, mac)) {
|
||||
throw new IOException("Invalid MAC");
|
||||
}
|
||||
|
||||
return new APDUResponse(plainData);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the SecureChannel as closed
|
||||
*/
|
||||
public void reset() {
|
||||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
|
||||
public class Select {
|
||||
private static final int CLA = 0x00;
|
||||
private static final int INS = 0xA4;
|
||||
private static final int P1 = 0x04;
|
||||
private static final int P2 = 0x00; // first occurrence
|
||||
|
||||
private byte[] aid;
|
||||
|
||||
public Select(byte[] aid) {
|
||||
this.aid = aid;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() {
|
||||
return new APDUCommand(CLA, INS, P1, P2, this.aid);
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
|
||||
public class Status {
|
||||
private static final int CLA = 0x80;
|
||||
private static final int INS = 0xF2;
|
||||
|
||||
public static final int P1_ISSUER_SECURITY_DOMAIN = 0x80;
|
||||
public static final int P1_APPLICATIONS = 0x40;
|
||||
public static final int P1_EXECUTABLE_LOAD_FILES = 0x20;
|
||||
public static final int P1_EXECUTABLE_LOAD_FILES_AND_MODULES = 0x10;
|
||||
|
||||
private static final int P2 = 0x02;
|
||||
|
||||
private int p1;
|
||||
|
||||
public Status(int p1) {
|
||||
this.p1 = p1;
|
||||
}
|
||||
|
||||
public APDUCommand getCommand() {
|
||||
byte[] data = new byte[]{0x4F, 0x00};
|
||||
return new APDUCommand(CLA, INS, this.p1, P2, data, true);
|
||||
}
|
||||
}
|
||||
-441
@@ -1,441 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUException;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUResponse;
|
||||
import im.status.applet_installer_test.appletinstaller.CardChannel;
|
||||
import org.spongycastle.jce.interfaces.ECPrivateKey;
|
||||
import org.spongycastle.jce.interfaces.ECPublicKey;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
/**
|
||||
* 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, APDUException {
|
||||
secureChannel.unpairOthers(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
|
||||
* class itself.
|
||||
*
|
||||
* @param info the P1 of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse getStatus(byte info) throws IOException {
|
||||
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, getStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key
|
||||
* derivation support.
|
||||
*
|
||||
* @return whether public key derivation is supported or not
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public boolean getPublicKeyDerivationSupport() throws IOException {
|
||||
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
|
||||
byte[] data = resp.getData();
|
||||
return data[data.length - 1] != 0x00;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.io.CardListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class ActionRunner implements CardListener {
|
||||
public final static int ACTION_NONE = 0;
|
||||
public final static int ACTION_INSTALL = 1;
|
||||
public final static int ACTION_INSTALL_TEST = 2;
|
||||
public final static int ACTION_PERFTEST = 3;
|
||||
|
||||
private AssetManager assets;
|
||||
private String capPath;
|
||||
private int requestedAction;
|
||||
|
||||
|
||||
public ActionRunner(AssetManager assets, String capPath) {
|
||||
this.assets = assets;
|
||||
this.capPath = capPath;
|
||||
this.requestedAction = ACTION_NONE;
|
||||
}
|
||||
|
||||
public void requestAction(int actionRequested) {
|
||||
switch(actionRequested) {
|
||||
case ACTION_NONE:
|
||||
Logger.i("cancelling requested action");
|
||||
break;
|
||||
case ACTION_INSTALL:
|
||||
Logger.i("installation requested");
|
||||
break;
|
||||
case ACTION_INSTALL_TEST:
|
||||
Logger.i("installation with test secrets requested");
|
||||
break;
|
||||
case ACTION_PERFTEST:
|
||||
Logger.i("performance tests requested");
|
||||
break;
|
||||
default:
|
||||
Logger.i("invalid action requested, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestedAction = actionRequested;
|
||||
}
|
||||
|
||||
private void perform(CardChannel ch) {
|
||||
Logger.i("starting requested action");
|
||||
try {
|
||||
if (!ch.isConnected()) {
|
||||
Logger.i("tag disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestedAction) {
|
||||
case ACTION_INSTALL:
|
||||
Installer installer = new Installer(ch, this.assets, this.capPath, false);
|
||||
installer.start();
|
||||
break;
|
||||
case ACTION_INSTALL_TEST:
|
||||
installer = new Installer(ch, this.assets, this.capPath, true);
|
||||
installer.start();
|
||||
break;
|
||||
case ACTION_PERFTEST:
|
||||
PerfTest perfTest = new PerfTest(ch);
|
||||
perfTest.test();
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown action");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Logger.e("IO exception: " + e.getMessage());
|
||||
} catch (APDUException e) {
|
||||
Logger.e("APDU exception: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
Logger.e("Other exception: " + e.getMessage());
|
||||
} finally {
|
||||
this.requestedAction = ACTION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(final CardChannel channel) {
|
||||
Logger.i("card connected");
|
||||
if (this.requestedAction != ACTION_NONE) {
|
||||
Logger.i("waiting 2 seconds to start requested action");
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
perform(channel);
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
Logger.i("no action requested yet");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
Logger.i("card disconnected");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import im.status.keycard.applet.KeycardCommandSet;
|
||||
import im.status.keycard.globalplatform.GlobalPlatformCommandSet;
|
||||
import im.status.keycard.globalplatform.LoadCallback;
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
public class Installer {
|
||||
private CardChannel plainChannel;
|
||||
private AssetManager assets;
|
||||
private String capPath;
|
||||
|
||||
private GlobalPlatformCommandSet cmdSet;
|
||||
|
||||
private boolean testSecrets;
|
||||
|
||||
public Installer(CardChannel channel, AssetManager assets, String capPath, boolean testSecrets) {
|
||||
this.plainChannel = channel;
|
||||
this.assets = assets;
|
||||
this.capPath = capPath;
|
||||
this.testSecrets = testSecrets;
|
||||
}
|
||||
|
||||
public void start() throws IOException, APDUException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
Logger.i("installation started...");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Logger.i("select ISD...");
|
||||
cmdSet = new GlobalPlatformCommandSet(this.plainChannel);
|
||||
cmdSet.select().checkOK();
|
||||
|
||||
Logger.i("opening secure channel...");
|
||||
cmdSet.openSecureChannel();
|
||||
|
||||
Logger.i("deleting old version (if present)...");
|
||||
cmdSet.deleteKeycardInstancesAndPackage();
|
||||
|
||||
Logger.i("loading package...");
|
||||
cmdSet.loadKeycardPackage(this.assets.open(this.capPath), new LoadCallback() {
|
||||
public void blockLoaded(int loadedBlock, int blockCount) {
|
||||
Logger.i(String.format("load %d/%d...", loadedBlock, blockCount));
|
||||
}
|
||||
});
|
||||
|
||||
Logger.i("installing NDEF applet...");
|
||||
cmdSet.installNDEFApplet(Hex.decode("0024d40f12616e64726f69642e636f6d3a706b67696d2e7374617475732e657468657265756d")).checkOK();
|
||||
|
||||
Logger.i("installing Keycard applet...");
|
||||
cmdSet.installKeycardApplet().checkOK();
|
||||
|
||||
if (testSecrets) {
|
||||
this.personalizeApplet();
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
Logger.i(String.format("\n\ninstallation completed in %d seconds", duration / 1000));
|
||||
}
|
||||
|
||||
private void personalizeApplet() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException {
|
||||
Secrets secrets = Secrets.testSecrets();
|
||||
|
||||
KeycardCommandSet cmdSet = new KeycardCommandSet(this.plainChannel);
|
||||
cmdSet.select().checkOK();
|
||||
cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword()).checkOK();
|
||||
|
||||
Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
interface UILogger {
|
||||
public void log(String m);
|
||||
}
|
||||
|
||||
public class Logger {
|
||||
private static UILogger uiLogger;
|
||||
private static int Level = Log.VERBOSE;
|
||||
private static int UILevel = Log.VERBOSE;
|
||||
|
||||
public static void setUILogger(UILogger l) {
|
||||
uiLogger = l;
|
||||
}
|
||||
|
||||
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(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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.AssetManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import de.cotech.sweetspot.NfcSweetspotData;
|
||||
import im.status.keycard.android.NFCCardManager;
|
||||
import de.cotech.sweetspot.ShowNfcSweetspotActivity;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements UILogger {
|
||||
private NfcAdapter nfcAdapter;
|
||||
private TextView textView;
|
||||
private ScrollView textViewScroll;
|
||||
|
||||
private Button buttonInstall;
|
||||
private Button buttonInstallTest;
|
||||
private Button buttonPerfTest;
|
||||
private Button buttonNFCSpot;
|
||||
private ActionRunner actionRunner;
|
||||
private NFCCardManager cardManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
Logger.setUILogger(this);
|
||||
|
||||
AssetManager assets = this.getAssets();
|
||||
this.actionRunner = new ActionRunner(assets, "keycard.cap");
|
||||
this.cardManager = new NFCCardManager();
|
||||
this.cardManager.setCardListener(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.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
requestAction(ActionRunner.ACTION_INSTALL);
|
||||
}
|
||||
});
|
||||
buttonInstallTest = (Button) findViewById(R.id.buttonInstallTest);
|
||||
buttonInstallTest.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
requestAction(ActionRunner.ACTION_INSTALL_TEST);
|
||||
}
|
||||
});
|
||||
buttonPerfTest = (Button) findViewById(R.id.buttonPerfTest);
|
||||
buttonPerfTest.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
requestAction(ActionRunner.ACTION_PERFTEST);
|
||||
}
|
||||
});
|
||||
buttonNFCSpot = (Button) findViewById(R.id.buttonNFCSpot);
|
||||
buttonNFCSpot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(MainActivity.this, ShowNfcSweetspotActivity.class);
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
|
||||
if (NfcSweetspotData.hasSweetspotData()) {
|
||||
buttonNFCSpot.setEnabled(true);
|
||||
}
|
||||
|
||||
//Logger.setUILevel(Log.INFO);
|
||||
//Logger.setLevel(Log.INFO);
|
||||
}
|
||||
|
||||
private void logException(Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) {
|
||||
msg = "exception without message";
|
||||
}
|
||||
|
||||
Logger.e("exception: " + msg);
|
||||
}
|
||||
|
||||
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.cardManager,
|
||||
NfcAdapter.FLAG_READER_NFC_A |
|
||||
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (nfcAdapter != null) {
|
||||
nfcAdapter.disableReaderMode(this);
|
||||
}
|
||||
}
|
||||
|
||||
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("");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.applet.KeycardCommandSet;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class PerfTest {
|
||||
private CardChannel cardChannel;
|
||||
private KeycardCommandSet cmdSet;
|
||||
|
||||
private long openSecureChannelTime = 0;
|
||||
private long loadKeysTime = 0;
|
||||
private long loginTime = 0;
|
||||
private long signTime = 0;
|
||||
private long getStatusTime = 0;
|
||||
|
||||
static final String BIP44_WALLET_PATH = "m/44'/60'/0'/0/0";
|
||||
|
||||
public PerfTest(CardChannel cardChannel) {
|
||||
this.cardChannel = cardChannel;
|
||||
}
|
||||
|
||||
public void test() throws Exception {
|
||||
cmdSet = new KeycardCommandSet(cardChannel);
|
||||
cmdSet.select().checkOK();
|
||||
|
||||
cmdSet.autoPair(cmdSet.pairingPasswordToSecret(Secrets.testSecrets().getPairingPassword()));
|
||||
openSecureChannelTime = System.currentTimeMillis();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
openSecureChannelTime = System.currentTimeMillis() - openSecureChannelTime;
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.unpairOthers(); // Recover in case of non-clean termination
|
||||
Logger.i("Measuring performances. Logging disabled. Please wait");
|
||||
Logger.setLevel(Log.INFO);
|
||||
|
||||
try {
|
||||
loadKeys();
|
||||
getStatus();
|
||||
login();
|
||||
signTransactions();
|
||||
} finally {
|
||||
Logger.setLevel(Log.INFO);
|
||||
Logger.setUILevel(Log.INFO);
|
||||
}
|
||||
|
||||
Logger.i("Reenabling logging.");
|
||||
cmdSet.select().checkOK();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.autoUnpair();
|
||||
Logger.i("*************************************************");
|
||||
Logger.i("Opening Secure Channel: " + openSecureChannelTime + "ms");
|
||||
Logger.i("Derivation of m/44'/60'/0'/0/0 from master: " + loadKeysTime + "ms");
|
||||
Logger.i("All following measurements are from application selection to the last needed APDU");
|
||||
Logger.i("GET STATUS: " + getStatusTime + "ms");
|
||||
Logger.i("Login: " + loginTime + "ms");
|
||||
Logger.i("Transaction signature: " + signTime + "ms");
|
||||
}
|
||||
|
||||
private void getStatus() throws Exception {
|
||||
long time = System.currentTimeMillis();
|
||||
cmdSet.select().checkOK();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK();
|
||||
getStatusTime = System.currentTimeMillis() - time;
|
||||
}
|
||||
|
||||
private void login() throws Exception {
|
||||
long time = System.currentTimeMillis();
|
||||
cmdSet.select().checkOK();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.exportKey("m/43'/60'/1581'/0'/0",false, false).checkOK();
|
||||
cmdSet.exportKey("m/43'/60'/1581'/1'/0",false, false).checkOK();
|
||||
loginTime = System.currentTimeMillis() - time;
|
||||
}
|
||||
|
||||
private void loadKeys() throws Exception {
|
||||
byte[] seed = new byte[64];
|
||||
new SecureRandom().nextBytes(seed);
|
||||
|
||||
cmdSet.loadKey(seed).checkOK();
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
cmdSet.deriveKey(BIP44_WALLET_PATH).checkOK();
|
||||
loadKeysTime = System.currentTimeMillis() - time;
|
||||
}
|
||||
|
||||
private void signTransactions() throws Exception {
|
||||
long time = System.currentTimeMillis();
|
||||
cmdSet.select().checkOK();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
cmdSet.sign("any32bytescanbeahashyouknowthat!".getBytes()).checkOK();
|
||||
signTime = System.currentTimeMillis() - time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package im.status.keycard.installer;
|
||||
|
||||
import android.util.Base64;
|
||||
import im.status.keycard.globalplatform.Crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static android.util.Base64.NO_PADDING;
|
||||
import static im.status.keycard.globalplatform.Crypto.randomLong;
|
||||
|
||||
public class Secrets {
|
||||
private String pin;
|
||||
private String puk;
|
||||
private String pairingPassword;
|
||||
|
||||
public Secrets(String pin, String puk, String pairingPassword) {
|
||||
this.pin = pin;
|
||||
this.puk = puk;
|
||||
this.pairingPassword = pairingPassword;
|
||||
}
|
||||
|
||||
public static Secrets generate() {
|
||||
String pairingPassword = randomToken(12);
|
||||
long pinNumber = randomLong(Crypto.PIN_BOUND);
|
||||
long pukNumber = randomLong(Crypto.PUK_BOUND);
|
||||
String pin = String.format("%06d", pinNumber);
|
||||
String puk = String.format("%012d", pukNumber);
|
||||
|
||||
return new Secrets(pin, puk, pairingPassword);
|
||||
}
|
||||
|
||||
public static Secrets testSecrets() {
|
||||
String pairingPassword = "KeycardTest";
|
||||
return new Secrets("000000", "123456789012", pairingPassword);
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public String getPuk() {
|
||||
return puk;
|
||||
}
|
||||
|
||||
public String getPairingPassword() {
|
||||
return pairingPassword;
|
||||
}
|
||||
|
||||
public static byte[] randomBytes(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte data[] = new byte[length];
|
||||
random.nextBytes(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static String randomToken(int length) {
|
||||
return Base64.encodeToString(randomBytes(length), NO_PADDING);
|
||||
}
|
||||
}
|
||||
@@ -7,35 +7,44 @@
|
||||
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:textIsSelectable="true" />
|
||||
|
||||
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 w/o INIT"/>
|
||||
|
||||
<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" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonNFCSpot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="NFC Spot"
|
||||
android:enabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">AppletInstaller</string>
|
||||
<string name="app_name">KeycardInstaller</string>
|
||||
</resources>
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class APDUCommandTest {
|
||||
@Test
|
||||
public void serialize() throws IOException {
|
||||
int cla = 0x80;
|
||||
int ins = 0x50;
|
||||
int p1 = 0;
|
||||
int p2 = 0;
|
||||
byte[] data = HexUtils.hexStringToByteArray("84762336c5187fe8");
|
||||
|
||||
APDUCommand c = new APDUCommand(cla, ins, p1, p2, (byte[])data, true);
|
||||
String expected = "805000000884762336C5187FE800";
|
||||
String actual = HexUtils.byteArrayToHexString(c.serialize());
|
||||
assertEquals(expected, actual);
|
||||
|
||||
c = new APDUCommand(cla, ins, p1, p2, (byte[])data);
|
||||
expected = "805000000884762336C5187FE8";
|
||||
actual = HexUtils.byteArrayToHexString(c.serialize());
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class APDUResponseTest {
|
||||
@Test
|
||||
public void parsing() {
|
||||
byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a69000");
|
||||
APDUResponse resp = new APDUResponse(apdu);
|
||||
|
||||
assertEquals(0x9000, resp.getSw());
|
||||
assertEquals(0x90, resp.getSw1());
|
||||
assertEquals(0x00, resp.getSw2());
|
||||
|
||||
String expected = "000002650183039536622002003B5E508F751C0AF3016E3FBC23D3A6";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(resp.getData()));
|
||||
assertTrue(resp.isOK());
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class APDUWrapperTest {
|
||||
@Test
|
||||
public void wrap() throws IOException {
|
||||
byte[] macKeyData = HexUtils.hexStringToByteArray("2983BA77D709C2DAA1E6000ABCCAC951");
|
||||
byte[] data = HexUtils.hexStringToByteArray("1d4de92eaf7a2c9f");
|
||||
|
||||
APDUCommand cmd = new APDUCommand(0x84, 0x82, 0x01, 0x00, data);
|
||||
APDUWrapper w = new APDUWrapper(macKeyData);
|
||||
|
||||
// check null icv
|
||||
assertEquals(HexUtils.byteArrayToHexString(Crypto.NullBytes8), HexUtils.byteArrayToHexString(w.getICV()));
|
||||
|
||||
APDUCommand wrapped = w.wrap(cmd);
|
||||
byte[] result = wrapped.serialize();
|
||||
String expected = "84820100101D4DE92EAF7A2C9F8F9B0DF681C1D3EC";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(result));
|
||||
|
||||
// second command
|
||||
|
||||
|
||||
// check icv from previous mac
|
||||
assertEquals("8F9B0DF681C1D3EC", HexUtils.byteArrayToHexString(w.getICV()));
|
||||
|
||||
data = HexUtils.hexStringToByteArray("4F00");
|
||||
cmd = new APDUCommand(0x80, 0xF2, 0x80, 0x02, data, true);
|
||||
|
||||
wrapped = w.wrap(cmd);
|
||||
result = wrapped.serialize();
|
||||
expected = "84F280020A4F0030F149209E17B39700";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(result));
|
||||
|
||||
// third command
|
||||
|
||||
|
||||
// check icv from previous mac
|
||||
assertEquals("30F149209E17B397", HexUtils.byteArrayToHexString(w.getICV()));
|
||||
|
||||
data = HexUtils.hexStringToByteArray("4F00");
|
||||
cmd = new APDUCommand(0x80, 0xF2, 0x40, 0x02, data, true);
|
||||
|
||||
wrapped = w.wrap(cmd);
|
||||
result = wrapped.serialize();
|
||||
expected = "84F240020A4F000D9B2D4E4365B5BD00";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(result));
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import im.status.applet_installer_test.appletinstaller.apducommands.InitializeUpdate;
|
||||
|
||||
public class CryptoTest {
|
||||
@Test
|
||||
public void deriveKey() {
|
||||
byte[] cardKey = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
|
||||
byte[] seq = HexUtils.hexStringToByteArray("0065");
|
||||
|
||||
byte[] encKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_ENC);
|
||||
String expectedEncKey = "85E72AAF47874218A202BF5EF891DD21";
|
||||
assertEquals(expectedEncKey, HexUtils.byteArrayToHexString(encKey));
|
||||
|
||||
byte[] macKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_MAC);
|
||||
String expectedMacKey = "309CF99E164F3A97F3E5017FF540A79F";
|
||||
assertEquals(expectedMacKey, HexUtils.byteArrayToHexString(macKey));
|
||||
|
||||
byte[] dekKey = Crypto.deriveKey(cardKey, seq, InitializeUpdate.DERIVATION_PURPOSE_DEK);
|
||||
String expectedDekKey = "93D08F8025242C4D775D69B9F16C939B";
|
||||
assertEquals(expectedDekKey, HexUtils.byteArrayToHexString(dekKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendDESPadding() {
|
||||
byte[] data = HexUtils.hexStringToByteArray("AABB");
|
||||
byte[] result = Crypto.appendDESPadding(data);
|
||||
String expected = "AABB800000000000";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyCryptogram() {
|
||||
byte[] encKey = HexUtils.hexStringToByteArray("16B5867FF50BE7239C2BF1245B83A362");
|
||||
byte[] hostChallenge = HexUtils.hexStringToByteArray("32da078d7aac1cff");
|
||||
byte[] cardChallenge = HexUtils.hexStringToByteArray("007284f64a7d6465");
|
||||
byte[] cardCryptogram = HexUtils.hexStringToByteArray("05c4bb8a86014e22");
|
||||
assertTrue(Crypto.verifyCryptogram(encKey, hostChallenge, cardChallenge, cardCryptogram));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void macFull3des() {
|
||||
byte[] key = HexUtils.hexStringToByteArray("5b02e75ad63190aece0622936f11abab");
|
||||
byte[] data = HexUtils.hexStringToByteArray("8482010010810b098a8fbb88da800000");
|
||||
String expected = "5271D7174A5A166A";
|
||||
|
||||
byte[] result = Crypto.macFull3des(key, data, Crypto.NullBytes8);
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(result));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class TlvTest {
|
||||
@Test
|
||||
public void find() throws IOException {
|
||||
byte[] data = HexUtils.hexStringToByteArray("C102BBCCC203DDEE11C30201");
|
||||
Tlv tlv = new Tlv(data);
|
||||
|
||||
Tlv child1 = tlv.find((byte) 0xC1);
|
||||
String expected = "BBCC";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(child1.getValue()));
|
||||
|
||||
Tlv child2 = tlv.find((byte) 0xC2);
|
||||
expected = "DDEE11";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(child2.getValue()));
|
||||
|
||||
|
||||
// tag exists, length is bigger than available data
|
||||
assertNull(tlv.find((byte) 0xC3));
|
||||
// not found
|
||||
assertNull(tlv.find((byte) 0xFF));
|
||||
// not found
|
||||
assertNull(child2.find((byte) 0xFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAID() throws IOException {
|
||||
byte[] data = HexUtils.hexStringToByteArray("6f5c8408a000000151000000a550734a06072a864886fc6b01600c060a2a864886fc6b02020201630906072a864886fc6b03640b06092a864886fc6b040255650b06092a864886fc6b020103660c060a2b060104012a026e01039f6501ff9000");
|
||||
Tlv tlv = new Tlv(data);
|
||||
|
||||
Tlv child1 = tlv.find((byte) 0x6F);
|
||||
assertNotNull(child1);
|
||||
|
||||
|
||||
Tlv child2 = child1.find((byte) 0x84);
|
||||
assertNotNull(child2);
|
||||
|
||||
String expected = "A000000151000000";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(child2.getValue()));
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class ExternalAuthenticateTest {
|
||||
@Test
|
||||
public void getHostCryptogram() {
|
||||
byte[] encKeyData = HexUtils.hexStringToByteArray("0EF72A1065236DD6CAC718D5E3F379A4");
|
||||
byte[] cardChallenge = HexUtils.hexStringToByteArray("0076a6c0d55e9535");
|
||||
byte[] hostChallenge = HexUtils.hexStringToByteArray("266195e638da1b95");
|
||||
|
||||
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
|
||||
|
||||
String expectedHostCryptogram = "45A5F48DAE68203C";
|
||||
byte[] hostCryptogram = auth.getHostCryptogram();
|
||||
assertEquals(expectedHostCryptogram, HexUtils.byteArrayToHexString(hostCryptogram));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCommand() throws IOException {
|
||||
byte[] encKeyData = HexUtils.hexStringToByteArray("8D289AFE0AB9C45B1C76DEEA182966F4");
|
||||
byte[] cardChallenge = HexUtils.hexStringToByteArray("000f3fd65d4d6e45");
|
||||
byte[] hostChallenge = HexUtils.hexStringToByteArray("cf307b6719bf224d");
|
||||
|
||||
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
|
||||
|
||||
String expectedAPDU = "84820100087702AC6CE46A47F0";
|
||||
byte[] apdu = auth.getCommand().serialize();
|
||||
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
}
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUException;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUResponse;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
import im.status.applet_installer_test.appletinstaller.Keys;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class InitializeUpdateTest {
|
||||
@Test
|
||||
public void getCommand() throws IOException {
|
||||
byte[] challenge = HexUtils.hexStringToByteArray("2d315d5ffc616d10");
|
||||
InitializeUpdate init = new InitializeUpdate(challenge);
|
||||
APDUCommand cmd = init.getCommand();
|
||||
|
||||
assertEquals(0x80, cmd.getCla());
|
||||
assertEquals(0x50, cmd.getIns());
|
||||
assertEquals(0, cmd.getP1());
|
||||
assertEquals(0, cmd.getP2());
|
||||
assertEquals(challenge, cmd.getData());
|
||||
|
||||
String expectedAPDU = "80500000082D315D5FFC616D1000";
|
||||
byte[] apdu = cmd.serialize();
|
||||
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateResponse_BadResponse() throws APDUException {
|
||||
byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a66982");
|
||||
APDUResponse resp = new APDUResponse(apdu);
|
||||
|
||||
byte[] challenge = InitializeUpdate.generateChallenge();
|
||||
InitializeUpdate init = new InitializeUpdate(challenge);
|
||||
|
||||
try {
|
||||
init.verifyResponse(new Keys(new byte[]{}, new byte[]{}), resp);
|
||||
fail("expected APDUException to be thrown");
|
||||
} catch (APDUException e) {
|
||||
assertEquals(0x6982, e.sw);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: reimplement test
|
||||
//@Test
|
||||
//public void validateResponse_GoodResponse() throws APDUException {
|
||||
// byte[] encKey = HexUtils.hexStringToByteArray("16B5867FF50BE7239C2BF1245B83A362");
|
||||
|
||||
// byte[] challenge = HexUtils.hexStringToByteArray("f0467f908e5ca23f");
|
||||
// InitializeUpdate init = new InitializeUpdate(challenge);
|
||||
|
||||
// byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002000de9c62ba1c4c8e55fcb91b6654ce49000");
|
||||
// APDUResponse resp = new APDUResponse(apdu);
|
||||
|
||||
// init.verifyResponse(new Keys(), resp);
|
||||
//}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommandTest;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class InstallForInstallTest {
|
||||
@Test
|
||||
public void forInstall() throws IOException {
|
||||
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
|
||||
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
|
||||
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
|
||||
byte[] params = HexUtils.hexStringToByteArray("AABBCC");
|
||||
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params);
|
||||
APDUCommand cmd = install.getCommand();
|
||||
byte[] apdu = cmd.serialize();
|
||||
|
||||
String expected = "80E60C00360C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C6574417070010005C903AABBCC00";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class InstallForLoadTest {
|
||||
@Test
|
||||
public void forLoad() throws IOException {
|
||||
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
|
||||
byte[] sdaid = HexUtils.hexStringToByteArray("A000000151000000");
|
||||
InstallForLoad install = new InstallForLoad(aid, sdaid);
|
||||
APDUCommand cmd = install.getCommand();
|
||||
byte[] apdu = cmd.serialize();
|
||||
|
||||
String expected = "80E60200190C53746174757357616C6C657408A000000151000000000000";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class LoadTest {
|
||||
|
||||
@Test
|
||||
public void getCommand() throws IOException {
|
||||
URL url = this.getClass().getClassLoader().getResource("wallet.cap");
|
||||
Load load = new Load(url.getPath());
|
||||
|
||||
ArrayList<APDUCommand> commands = new ArrayList<APDUCommand>();
|
||||
APDUCommand cmd;
|
||||
while((cmd = load.getCommand()) != null) {
|
||||
commands.add(cmd);
|
||||
}
|
||||
|
||||
assertEquals(31, commands.size());
|
||||
|
||||
// Command 1
|
||||
cmd = commands.get(0);
|
||||
assertEquals(0, cmd.getP1());
|
||||
assertEquals(0, cmd.getP2());
|
||||
String expectedData = "C4821D74010027DECAFFED02020401010C53746174757357616C6C657410696D2F7374617475732F77616C6C6574020021002700210013002902B600401581015D0301000006EB372C0024000A013504010004002904000107A0000000620001050107A0000000620102050107A0000000620101050107A0000000620201030013010F53746174757357616C6C65744170700937060040000000800000FF000100000000800000FF00010000000080000D000A010A000004EB05A005F307140739080C08D608EB0908090D008203160010070100000A7B07158106005A801A0076003003808009038B00300457800904620030051F8010";
|
||||
byte[] data = cmd.getData();
|
||||
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
|
||||
|
||||
// Command 2
|
||||
cmd = commands.get(1);
|
||||
assertEquals(0, cmd.getP1());
|
||||
assertEquals(1, cmd.getP2());
|
||||
expectedData = "053100440A9380AB0B4000750EBF80930F5400300110188C002F7A0302058D00327F003307038D00347F003506038D00377F00381006038D00347F003B032F101B038D00407F004110141020038D0042940000437F005E700B2C017F00411100802F10651C41048D005F7F00607A0861032906181D2510805310806B1E7B00601606590601033816061A7B006016068E03006113412906702B1B7B0060038E03006213044329067B0060037B00601606250453600506700305381606054704412906181D7B00601606078D006329061504160510207B00600316067B006016068D00647B006016067B0065038D0066620403781A7B0060";
|
||||
data = cmd.getData();
|
||||
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
|
||||
|
||||
// Last command
|
||||
cmd = commands.get(30);
|
||||
assertEquals(0x80, cmd.getP1());
|
||||
assertEquals(30, cmd.getP2());
|
||||
expectedData = "080707080C08090A04050A0A06070706081A07085029031512201209190C0B0B0503060D09030E0B0C0C080A0A0603070706104622070914240A1611130D080B0F0A0D10110905040B2E271205030E0D0D0D0D0807140808030E1E10050321032C2A070606080D140C2723180B081D0A0707060811030D0407070608201B07091408252C2E39";
|
||||
data = cmd.getData();
|
||||
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SelectTest {
|
||||
@Test
|
||||
public void getCode() throws IOException {
|
||||
Select s = new Select(new byte[0]);
|
||||
byte[] apdu = s.getCommand().serialize();
|
||||
String expected = "00A4040000";
|
||||
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package im.status.applet_installer_test.appletinstaller.apducommands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.applet_installer_test.appletinstaller.APDUCommand;
|
||||
import im.status.applet_installer_test.appletinstaller.APDUWrapper;
|
||||
import im.status.applet_installer_test.appletinstaller.HexUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class StatusTest {
|
||||
@Test
|
||||
public void getCommand() throws IOException {
|
||||
Status status = new Status(Status.P1_ISSUER_SECURITY_DOMAIN);
|
||||
String expectedAPDU = "80F28002024F0000";
|
||||
byte[] apdu = status.getCommand().serialize();
|
||||
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
@@ -1 +1,2 @@
|
||||
include ':app'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user