Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7c4ba3bc | ||
|
|
152d7f8e34 | ||
|
|
ee41722527 | ||
|
|
f4dd1d17cf | ||
|
|
425d085716 | ||
|
|
b249bd75e6 | ||
|
|
f603979cb5 | ||
|
|
db3eee0d76 | ||
|
|
99dbb24067 | ||
|
|
f9df2be80f | ||
|
|
07cba4aa9d | ||
|
|
527efc7a4e | ||
|
|
067204c7db | ||
|
|
2df15f388c | ||
|
|
93d71ab70b | ||
|
|
50fa93dd32 | ||
|
|
bbe3693e4e | ||
|
|
b5ff6d033e | ||
|
|
93ff092a14 | ||
|
|
f6f9a65be1 | ||
|
|
0d16541875 | ||
|
|
171220bd79 | ||
|
|
b08b1dc7ce | ||
|
|
07fc087cb3 | ||
|
|
c749e5a744 | ||
|
|
02fee8edd2 | ||
|
|
c31df742dd | ||
|
|
3c759152e3 | ||
|
|
735ccd80d9 | ||
|
|
439237a75a | ||
|
|
5a2204bbcb | ||
|
|
918aec72ac | ||
|
|
145fe4b554 | ||
|
|
1575dc9d86 | ||
|
|
ae65a94705 | ||
|
|
a9726f3c50 | ||
|
|
a85e1de2e6 | ||
|
|
3cad21254c | ||
|
|
e781d5551f | ||
|
|
1ae29cd1cd | ||
|
|
346ab46187 | ||
|
|
41f2473f64 | ||
|
|
4399be35a4 | ||
|
|
ed7630bada | ||
|
|
03b0a56a09 | ||
|
|
a66f921ee8 | ||
|
|
801accdde3 | ||
|
|
002ade1c74 | ||
|
|
f646e5d8ee | ||
|
|
931b6608d5 | ||
|
|
70b50c8989 | ||
|
|
8b4e66e50b | ||
|
|
f4fd086467 | ||
|
|
eb9f3f1350 |
@@ -1,9 +1,7 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Keycard Java SDK for Android and Desktop
|
||||
|
||||
This SDK simplifies integration with the [Status Keycard](https://github.com/status-im/status-keycard) in Android
|
||||
and Desktop applications. In this SDK you find both the classes needed for generic communication with SmartCards as well
|
||||
as classes specifically addressing the Keycard.
|
||||
|
||||
To get started, check the file ```demo-android/src/main/java/im/status/keycard/app/MainActivity.java``` which a simple
|
||||
demo application showing how the SDK works and what you can do with it.
|
||||
|
||||
## Usage
|
||||
|
||||
You can import the SDK in your Gradle or Maven project using [Jitpack.io](https://jitpack.io).
|
||||
|
||||
### On Android
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'com.github.status-im.status-keycard-java:android:2.1.1'
|
||||
}
|
||||
```
|
||||
|
||||
### on the desktop
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'com.github.status-im.status-keycard-java:desktop:2.1.1'
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
@@ -0,0 +1,59 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
|
||||
group = 'com.github.status-im'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 201
|
||||
versionName "2.0.1"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':lib')
|
||||
}
|
||||
|
||||
// build a jar with source files
|
||||
task sourcesJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier = 'sources'
|
||||
}
|
||||
|
||||
task javadoc(type: Javadoc) {
|
||||
failOnError false
|
||||
source = android.sourceSets.main.java.sourceFiles
|
||||
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
|
||||
classpath += configurations.compile
|
||||
}
|
||||
|
||||
// build a jar with javadoc
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
|
||||
android.libraryVariants.all { variant ->
|
||||
def name = variant.buildType.name
|
||||
def task = project.tasks.create "jar${name.capitalize()}", Jar
|
||||
task.dependsOn variant.javaCompile
|
||||
task.from variant.javaCompile.destinationDir
|
||||
artifacts.add('archives', task);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.status.hardwallet_lite_android">
|
||||
package="im.status.keycard">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,37 @@
|
||||
package im.status.keycard.android;
|
||||
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Implementation of the CardChannel interface using the Android NFC API.
|
||||
*/
|
||||
public class NFCCardChannel implements CardChannel {
|
||||
private static final String TAG = "CardChannel";
|
||||
|
||||
private IsoDep isoDep;
|
||||
|
||||
public NFCCardChannel(IsoDep isoDep) {
|
||||
this.isoDep = isoDep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
byte[] apdu = cmd.serialize();
|
||||
Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
|
||||
byte[] resp = this.isoDep.transceive(apdu);
|
||||
APDUResponse response = new APDUResponse(resp);
|
||||
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return this.isoDep.isConnected();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package im.status.keycard.android;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import im.status.keycard.globalplatform.Crypto;
|
||||
import im.status.keycard.io.CardListener;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
|
||||
* a runloop which monitors the connection and from which CardListener callbacks are called.
|
||||
*/
|
||||
public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback {
|
||||
private static final String TAG = "NFCCardManager";
|
||||
private static final int DEFAULT_LOOP_SLEEP_MS = 50;
|
||||
|
||||
private IsoDep isoDep;
|
||||
private boolean isRunning;
|
||||
private CardListener cardListener;
|
||||
private int loopSleepMS;
|
||||
|
||||
static {
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an NFC Card Manager with default delay between loop iterations.
|
||||
*/
|
||||
public NFCCardManager() {
|
||||
this(DEFAULT_LOOP_SLEEP_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an NFC Card Manager with the given delay between loop iterations.
|
||||
*
|
||||
* @param loopSleepMS time to sleep between loops
|
||||
*/
|
||||
public NFCCardManager(int loopSleepMS) {
|
||||
this.loopSleepMS = loopSleepMS;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if connected, false otherwise.
|
||||
* @return if connected, false otherwise
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return isoDep != null && isoDep.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagDiscovered(Tag tag) {
|
||||
isoDep = IsoDep.get(tag);
|
||||
try {
|
||||
isoDep = IsoDep.get(tag);
|
||||
isoDep.connect();
|
||||
isoDep.setTimeout(120000);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "error connecting to tag");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runloop. Do NOT invoke directly. Use start() instead.
|
||||
*/
|
||||
public void run() {
|
||||
boolean connected = isConnected();
|
||||
|
||||
while (true) {
|
||||
boolean newConnected = isConnected();
|
||||
if (newConnected != connected) {
|
||||
connected = newConnected;
|
||||
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
|
||||
|
||||
if (connected && !isRunning) {
|
||||
onCardConnected();
|
||||
} else {
|
||||
onCardDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
SystemClock.sleep(loopSleepMS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts on card connected by calling the callback of the registered listener.
|
||||
*/
|
||||
private void onCardConnected() {
|
||||
isRunning = true;
|
||||
|
||||
if (cardListener != null) {
|
||||
cardListener.onConnected(new NFCCardChannel(isoDep));
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts on card disconnected by calling the callback of the registered listener.
|
||||
*/
|
||||
private void onCardDisconnected() {
|
||||
isRunning = false;
|
||||
isoDep = null;
|
||||
if (cardListener != null) {
|
||||
cardListener.onDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the card listener.
|
||||
*
|
||||
* @param listener the new listener
|
||||
*/
|
||||
public void setCardListener(CardListener listener) {
|
||||
cardListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,3 +22,7 @@ allprojects {
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.withType(Javadoc).all { enabled = false }
|
||||
}
|
||||
@@ -3,11 +3,11 @@ apply plugin: 'com.android.application'
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
applicationId "im.status.hardwallet_lite_android.demo"
|
||||
applicationId "im.status.keycard.demo"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 201
|
||||
versionName "2.0.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
@@ -16,13 +16,23 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("lint-config.xml")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
|
||||
implementation project(':lib')
|
||||
implementation project(':android')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
@@ -0,0 +1,5 @@
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/bcprov-jdk15on-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.status.hardwallet_lite_android.demo">
|
||||
package="im.status.keycard.demo">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -9,7 +9,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name="im.status.hardwallet_lite_android.app.MainActivity">
|
||||
<activity android:name="im.status.keycard.app.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package im.status.keycard.app;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import im.status.keycard.demo.R;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.io.CardListener;
|
||||
import im.status.keycard.android.NFCCardManager;
|
||||
import im.status.keycard.applet.*;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private NFCCardManager cardManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
cardManager = new NFCCardManager();
|
||||
|
||||
cardManager.setCardListener(new CardListener() {
|
||||
@Override
|
||||
public void onConnected(CardChannel cardChannel) {
|
||||
try {
|
||||
// Applet-specific code
|
||||
KeycardCommandSet cmdSet = new KeycardCommandSet(cardChannel);
|
||||
|
||||
Log.i(TAG, "Applet selection successful");
|
||||
|
||||
// First thing to do is selecting the applet on the card.
|
||||
ApplicationInfo info = new ApplicationInfo(cmdSet.select().checkOK().getData());
|
||||
|
||||
// If the card is not initialized, the INIT apdu must be sent. The actual PIN, PUK and pairing password values
|
||||
// can be either generated or chosen by the user. Using fixed values is highly discouraged.
|
||||
if (!info.isInitializedCard()) {
|
||||
Log.i(TAG, "Initializing card with test secrets");
|
||||
cmdSet.init("000000", "123456789012", "KeycardTest").checkOK();
|
||||
info = new ApplicationInfo(cmdSet.select().checkOK().getData());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Instance UID: " + Hex.toHexString(info.getInstanceUID()));
|
||||
Log.i(TAG, "Secure channel public key: " + Hex.toHexString(info.getSecureChannelPubKey()));
|
||||
Log.i(TAG, "Application version: " + info.getAppVersionString());
|
||||
Log.i(TAG, "Free pairing slots: " + info.getFreePairingSlots());
|
||||
if (info.hasMasterKey()) {
|
||||
Log.i(TAG, "Key UID: " + Hex.toHexString(info.getKeyUID()));
|
||||
} else {
|
||||
Log.i(TAG, "The card has no master key");
|
||||
}
|
||||
Log.i(TAG, String.format("Capabilities: %02X", info.getCapabilities()));
|
||||
Log.i(TAG, "Has Secure Channel: " + info.hasSecureChannelCapability());
|
||||
Log.i(TAG, "Has Key Management: " + info.hasKeyManagementCapability());
|
||||
Log.i(TAG, "Has Credentials Management: " + info.hasCredentialsManagementCapability());
|
||||
Log.i(TAG, "Has NDEF capability: " + info.hasNDEFCapability());
|
||||
|
||||
if (info.hasSecureChannelCapability()) {
|
||||
// In real projects, the pairing key should be saved and used for all new sessions.
|
||||
cmdSet.autoPair("KeycardTest");
|
||||
Pairing pairing = cmdSet.getPairing();
|
||||
|
||||
// Never log the pairing key in a real application!
|
||||
Log.i(TAG, "Pairing with card is done.");
|
||||
Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
|
||||
Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
|
||||
|
||||
// Opening a Secure Channel is needed for all other applet commands
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
Log.i(TAG, "Secure channel opened. Getting applet status.");
|
||||
}
|
||||
|
||||
// We send a GET STATUS command, which does not require PIN authentication
|
||||
ApplicationStatus status = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
|
||||
|
||||
Log.i(TAG, "PIN retry counter: " + status.getPINRetryCount());
|
||||
Log.i(TAG, "PUK retry counter: " + status.getPUKRetryCount());
|
||||
Log.i(TAG, "Has master key: " + status.hasMasterKey());
|
||||
|
||||
if (info.hasKeyManagementCapability()) {
|
||||
// A mnemonic can be generated before PIN authentication. Generating a mnemonic does not create keys on the
|
||||
// card. a subsequent loadKey step must be performed after PIN authentication. In this example we will only
|
||||
// show how to convert the output of the card to a usable format but won't actually load the key
|
||||
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(KeycardCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
|
||||
|
||||
// We need to set a wordlist if we plan using this object to derive the binary seed. If we just need the word
|
||||
// indexes we can skip this step and call mnemonic.getIndexes() instead.
|
||||
mnemonic.fetchBIP39EnglishWordlist();
|
||||
|
||||
Log.i(TAG, "Generated mnemonic phrase: " + mnemonic.toMnemonicPhrase());
|
||||
Log.i(TAG, "Binary seed: " + Hex.toHexString(mnemonic.toBinarySeed()));
|
||||
}
|
||||
|
||||
if (info.hasCredentialsManagementCapability()) {
|
||||
// PIN authentication allows execution of privileged commands
|
||||
cmdSet.verifyPIN("000000").checkAuthOK();
|
||||
|
||||
Log.i(TAG, "Pin Verified.");
|
||||
}
|
||||
|
||||
// If the card has no keys, we generate a new set. Keys can also be loaded on the card starting from a binary
|
||||
// seed generated from a mnemonic phrase. In alternative, we could load the generated keypair as shown in the
|
||||
// commented line of code.
|
||||
if (!status.hasMasterKey() && info.hasKeyManagementCapability()) {
|
||||
cmdSet.generateKey();
|
||||
//cmdSet.loadKey(mnemonic.toBIP32KeyPair());
|
||||
}
|
||||
|
||||
// Get the current key path using GET STATUS
|
||||
KeyPath currentPath = new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData());
|
||||
Log.i(TAG, "Current key path: " + currentPath);
|
||||
|
||||
if (!currentPath.toString().equals("m/44'/0'/0'/0/0")) {
|
||||
// Key derivation is needed to select the desired key. The derived key remains current until a new derive
|
||||
// command is sent (it is not lost on power loss).
|
||||
cmdSet.deriveKey("m/44'/0'/0'/0/0").checkOK();
|
||||
Log.i(TAG, "Derived m/44'/0'/0'/0/0");
|
||||
}
|
||||
|
||||
// We retrieve the wallet public key
|
||||
BIP32KeyPair walletPublicKey = BIP32KeyPair.fromTLV(cmdSet.exportCurrentKey(true).checkOK().getData());
|
||||
|
||||
Log.i(TAG, "Wallet public key: " + Hex.toHexString(walletPublicKey.getPublicKey()));
|
||||
Log.i(TAG, "Wallet address: " + Hex.toHexString(walletPublicKey.toEthereumAddress()));
|
||||
|
||||
byte[] hash = "thiscouldbeahashintheorysoitisok".getBytes();
|
||||
|
||||
RecoverableSignature signature = new RecoverableSignature(hash, cmdSet.sign(hash).checkOK().getData());
|
||||
|
||||
Log.i(TAG, "Signed hash: " + Hex.toHexString(hash));
|
||||
Log.i(TAG, "Recovery ID: " + signature.getRecId());
|
||||
Log.i(TAG, "R: " + Hex.toHexString(signature.getR()));
|
||||
Log.i(TAG, "S: " + Hex.toHexString(signature.getS()));
|
||||
|
||||
if (info.hasSecureChannelCapability()) {
|
||||
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
|
||||
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
|
||||
// this would be a very bad idea to do.
|
||||
cmdSet.unpairOthers();
|
||||
cmdSet.autoUnpair();
|
||||
|
||||
Log.i(TAG, "Unpaired.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
Log.i(TAG, "Card disconnected.");
|
||||
}
|
||||
});
|
||||
cardManager.start();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".app.MainActivity">
|
||||
tools:context="im.status.keycard.app.MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">KeycardAndroid</string>
|
||||
</resources>
|
||||
@@ -1,95 +0,0 @@
|
||||
package im.status.hardwallet_lite_android.app;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import im.status.hardwallet_lite_android.demo.R;
|
||||
import im.status.hardwallet_lite_android.io.APDUResponse;
|
||||
import im.status.hardwallet_lite_android.io.CardChannel;
|
||||
import im.status.hardwallet_lite_android.io.CardManager;
|
||||
import im.status.hardwallet_lite_android.io.OnCardConnectedListener;
|
||||
import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private CardManager cardManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
cardManager = new CardManager();
|
||||
|
||||
cardManager.setOnCardConnectedListener(new OnCardConnectedListener() {
|
||||
@Override
|
||||
public void onConnected(CardChannel cardChannel) {
|
||||
try {
|
||||
|
||||
Log.i(TAG, "onCardConnected()");
|
||||
|
||||
// Applet-specific code
|
||||
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(cardChannel);
|
||||
|
||||
// First thing to do is selecting the applet on the card.
|
||||
cmdSet.select().checkOK();
|
||||
|
||||
Log.i(TAG, "Applet is installed on the connected card.");
|
||||
|
||||
// In real projects, the pairing key should be saved and used for all new sessions.
|
||||
cmdSet.autoPair("WalletAppletTest");
|
||||
|
||||
Log.i(TAG, "Pairing with card is done.");
|
||||
|
||||
// Opening a Secure Channel is needed for all other applet commands
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
Log.i(TAG, "Secure channel opened.");
|
||||
|
||||
// We send a GET STATUS command, which does not require PIN authentication
|
||||
APDUResponse resp = cmdSet.getStatus(WalletAppletCommandSet.GET_STATUS_P1_APPLICATION).checkOK();
|
||||
|
||||
Log.i(TAG, "Got status (response length=" + resp.getData().length + ")." );
|
||||
|
||||
// PIN authentication allows execution of privileged commands
|
||||
cmdSet.verifyPIN("000000").checkOK();
|
||||
|
||||
Log.i(TAG, "Pin Verified.");
|
||||
|
||||
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
|
||||
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
|
||||
// this would be a very bad idea to do.
|
||||
cmdSet.unpairOthers();
|
||||
cmdSet.autoUnpair();
|
||||
|
||||
Log.i(TAG, "Unpaired.");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
cardManager.start();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">HardwalletLiteAndroid</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
@@ -0,0 +1,22 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven'
|
||||
|
||||
dependencies {
|
||||
compile project(':lib')
|
||||
compile 'org.hid4java:hid4java:0.5.0'
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
classifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package im.status.keycard.desktop;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import org.hid4java.HidDevice;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class LedgerUSBChannel implements CardChannel {
|
||||
private static final int HID_BUFFER_SIZE = 64;
|
||||
private static final int LEDGER_DEFAULT_CHANNEL = 1;
|
||||
private static final int TAG_APDU = 0x05;
|
||||
private static final int READ_TIMEOUT = 20000;
|
||||
|
||||
private HidDevice hidDevice;
|
||||
|
||||
public LedgerUSBChannel(HidDevice hidDevice) {
|
||||
this.hidDevice = hidDevice;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
|
||||
int offset = 0;
|
||||
|
||||
byte[] command = wrapCommandAPDU(cmd.serialize());
|
||||
|
||||
byte[] chunk = new byte[HID_BUFFER_SIZE];
|
||||
|
||||
while(offset != command.length) {
|
||||
System.arraycopy(command, offset, chunk, 0, HID_BUFFER_SIZE);
|
||||
|
||||
if (hidDevice.write(chunk, HID_BUFFER_SIZE, (byte) 0x00) < 0) {
|
||||
throw new IOException("Write failed");
|
||||
}
|
||||
|
||||
offset += HID_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
byte[] responseData = null;
|
||||
|
||||
while ((responseData = unwrapResponseAPDU(response.toByteArray())) == null) {
|
||||
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
|
||||
throw new IOException("Read failed");
|
||||
}
|
||||
|
||||
response.write(chunk, 0, HID_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
return new APDUResponse(responseData);
|
||||
}
|
||||
|
||||
private byte[] unwrapResponseAPDU(byte[] data) throws IOException {
|
||||
if ((data == null) || (data.length < 7 + 5)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int sequenceIdx = 0;
|
||||
int offset = checkResponseHeader(data, 0, sequenceIdx);
|
||||
|
||||
int responseLength = ((data[offset++] & 0xff) << 8);
|
||||
responseLength |= (data[offset++] & 0xff);
|
||||
|
||||
if (data.length < 7 + responseLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
|
||||
int blockSize = (responseLength > HID_BUFFER_SIZE - 7 ? HID_BUFFER_SIZE - 7 : responseLength);
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
|
||||
while (response.size() != responseLength) {
|
||||
sequenceIdx++;
|
||||
|
||||
if (offset == data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
offset = checkResponseHeader(data, offset, sequenceIdx);
|
||||
|
||||
blockSize = (responseLength - response.size() > HID_BUFFER_SIZE - 5 ? HID_BUFFER_SIZE - 5 : responseLength - response.size());
|
||||
if (blockSize > data.length - offset) {
|
||||
return null;
|
||||
}
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
|
||||
return response.toByteArray();
|
||||
}
|
||||
|
||||
private int checkResponseHeader(byte[] data, int offset, int sequenceIdx) throws IOException {
|
||||
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL >> 8)) {
|
||||
throw new IOException("Invalid channel");
|
||||
}
|
||||
|
||||
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL & 0xff)) {
|
||||
throw new IOException("Invalid channel");
|
||||
}
|
||||
|
||||
if (data[offset++] != TAG_APDU) {
|
||||
throw new IOException("Invalid tag");
|
||||
}
|
||||
|
||||
if (data[offset++] != (sequenceIdx >> 8)) {
|
||||
throw new IOException("Invalid sequence");
|
||||
}
|
||||
|
||||
if (data[offset++] != (sequenceIdx & 0xff)) {
|
||||
throw new IOException("Invalid sequence");
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
private byte[] wrapCommandAPDU(byte[] command) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
int sequenceIdx = 0;
|
||||
int offset = 0;
|
||||
writeCommandHeader(output, sequenceIdx);
|
||||
sequenceIdx++;
|
||||
|
||||
output.write(command.length >> 8);
|
||||
output.write(command.length);
|
||||
int blockSize = (command.length > (HID_BUFFER_SIZE - 7) ? (HID_BUFFER_SIZE - 7) : command.length);
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
|
||||
while (offset != command.length) {
|
||||
writeCommandHeader(output, sequenceIdx);
|
||||
sequenceIdx++;
|
||||
|
||||
blockSize = ((command.length - offset) > (HID_BUFFER_SIZE - 5) ? (HID_BUFFER_SIZE - 5) : (command.length - offset));
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
|
||||
if ((output.size() % HID_BUFFER_SIZE) != 0) {
|
||||
byte[] padding = new byte[HID_BUFFER_SIZE - (output.size() % HID_BUFFER_SIZE)];
|
||||
output.write(padding, 0, padding.length);
|
||||
}
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx) {
|
||||
output.write(LEDGER_DEFAULT_CHANNEL >> 8);
|
||||
output.write(LEDGER_DEFAULT_CHANNEL);
|
||||
output.write(TAG_APDU);
|
||||
output.write(sequenceIdx >> 8);
|
||||
output.write(sequenceIdx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return hidDevice.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pairingPasswordPBKDF2IterationCount() {
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package im.status.keycard.desktop;
|
||||
|
||||
import im.status.keycard.globalplatform.Crypto;
|
||||
import im.status.keycard.io.CardListener;
|
||||
import org.hid4java.*;
|
||||
import org.hid4java.event.HidServicesEvent;
|
||||
|
||||
public class LedgerUSBManager implements HidServicesListener {
|
||||
static {
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
private static final int VID = 0x2c97;
|
||||
private static final int PID = 0x0001;
|
||||
private static final int SCAN_INTERVAL_MS = 500;
|
||||
private static final int PAUSE_INTERVAL_MS = 5000;
|
||||
|
||||
private HidServices hidServices;
|
||||
private CardListener listener;
|
||||
|
||||
public LedgerUSBManager(CardListener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
HidServicesSpecification hidServicesSpecification = new HidServicesSpecification();
|
||||
hidServicesSpecification.setAutoShutdown(true);
|
||||
hidServicesSpecification.setScanInterval(SCAN_INTERVAL_MS);
|
||||
hidServicesSpecification.setPauseInterval(PAUSE_INTERVAL_MS);
|
||||
hidServicesSpecification.setScanMode(ScanMode.SCAN_AT_FIXED_INTERVAL_WITH_PAUSE_AFTER_WRITE);
|
||||
|
||||
hidServices = HidManager.getHidServices(hidServicesSpecification);
|
||||
hidServices.addHidServicesListener(this);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
hidServices.start();
|
||||
|
||||
HidDevice hidDevice = hidServices.getHidDevice(VID, PID, null);
|
||||
|
||||
if (hidDevice != null) {
|
||||
listener.onConnected(new LedgerUSBChannel(hidDevice));
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
hidServices.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceAttached(HidServicesEvent event) {
|
||||
HidDevice hidDevice = event.getHidDevice();
|
||||
|
||||
if (hidDevice.isVidPidSerial(VID, PID, null)) {
|
||||
listener.onConnected(new LedgerUSBChannel(hidDevice));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceDetached(HidServicesEvent event) {
|
||||
hidFailure(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidFailure(HidServicesEvent event) {
|
||||
HidDevice hidDevice = event.getHidDevice();
|
||||
|
||||
if (hidDevice.isVidPidSerial(VID, PID, null)) {
|
||||
listener.onDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package im.status.keycard.desktop;
|
||||
|
||||
import im.status.keycard.globalplatform.Crypto;
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CommandAPDU;
|
||||
import javax.smartcardio.ResponseAPDU;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Implementation of a CardChannel using the Java Smartcard I/O API,
|
||||
*/
|
||||
public class PCSCCardChannel implements CardChannel {
|
||||
static {
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
private javax.smartcardio.CardChannel cardChannel;
|
||||
|
||||
/**
|
||||
* Constructor. Wraps a Java Smartcard I/O CardChannel.
|
||||
* @param cardChannel the card channel to wrap.
|
||||
*/
|
||||
public PCSCCardChannel(javax.smartcardio.CardChannel cardChannel) {
|
||||
this.cardChannel = cardChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
CommandAPDU capdu = new CommandAPDU(cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData(), cmd.getNeedsLE() ? 0x100 : 0x00);
|
||||
|
||||
ResponseAPDU rapdu;
|
||||
|
||||
try {
|
||||
rapdu = cardChannel.transmit(capdu);
|
||||
} catch (CardException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
return new APDUResponse(rapdu.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
task androidSourcesJar(type: Jar) {
|
||||
from android.sourceSets.main.java.source
|
||||
classifier = 'sources'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives androidSourcesJar
|
||||
}
|
||||
|
||||
}
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven'
|
||||
|
||||
dependencies {
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
classifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package im.status.hardwallet_lite_android.io;
|
||||
|
||||
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.hardwallet_lite_android.io;
|
||||
|
||||
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.hardwallet_lite_android.io;
|
||||
|
||||
public class APDUResponse {
|
||||
public static int SW_OK = 0x9000;
|
||||
public static int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
|
||||
public static int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
|
||||
public static int SW_CARD_LOCKED = 0x6283;
|
||||
public static int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
|
||||
public static int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
|
||||
|
||||
private byte[] apdu;
|
||||
private byte[] data;
|
||||
private int sw;
|
||||
private int sw1;
|
||||
private int sw2;
|
||||
|
||||
public APDUResponse(byte[] apdu) {
|
||||
if (apdu.length < 2) {
|
||||
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
|
||||
}
|
||||
this.apdu = apdu;
|
||||
this.parse();
|
||||
}
|
||||
|
||||
private void parse() {
|
||||
int length = this.apdu.length;
|
||||
|
||||
this.sw1 = this.apdu[length - 2] & 0xff;
|
||||
this.sw2 = this.apdu[length - 1] & 0xff;
|
||||
this.sw = (this.sw1 << 8) | this.sw2;
|
||||
|
||||
this.data = new byte[length - 2];
|
||||
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
|
||||
}
|
||||
|
||||
public boolean isOK() {
|
||||
return this.sw == SW_OK;
|
||||
}
|
||||
|
||||
public APDUResponse checkOK() throws APDUException {
|
||||
if (!isOK()) {
|
||||
throw new APDUException(this.getSw(), "Unexpected error SW");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public int getSw() {
|
||||
return this.sw;
|
||||
}
|
||||
|
||||
public int getSw1() {
|
||||
return this.sw1;
|
||||
}
|
||||
|
||||
public int getSw2() {
|
||||
return this.sw2;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return this.apdu;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package im.status.hardwallet_lite_android.io;
|
||||
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CardChannel {
|
||||
private static final String TAG = "CardChannel";
|
||||
|
||||
private IsoDep isoDep;
|
||||
|
||||
public CardChannel(IsoDep isoDep) {
|
||||
this.isoDep = isoDep;
|
||||
}
|
||||
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
byte[] apdu = cmd.serialize();
|
||||
Log.d(TAG, String.format("COMMAND %s", Hex.toHexString(apdu)));
|
||||
byte[] resp = this.isoDep.transceive(apdu);
|
||||
Log.d(TAG, String.format("RESPONSE %s %n-----------------------", Hex.toHexString(resp)));
|
||||
return new APDUResponse(resp);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package im.status.hardwallet_lite_android.io;
|
||||
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.security.Security;
|
||||
|
||||
public class CardManager extends Thread implements NfcAdapter.ReaderCallback {
|
||||
|
||||
public CardManager() {
|
||||
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
|
||||
}
|
||||
|
||||
private static final String TAG = "CardManager";
|
||||
|
||||
private IsoDep isoDep;
|
||||
private boolean isRunning;
|
||||
private OnCardConnectedListener onCardConnectedListener;
|
||||
|
||||
public boolean isConnected() {
|
||||
return isoDep != null && isoDep.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTagDiscovered(Tag tag) {
|
||||
isoDep = IsoDep.get(tag);
|
||||
|
||||
try {
|
||||
isoDep = IsoDep.get(tag);
|
||||
isoDep.connect();
|
||||
isoDep.setTimeout(120000);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "error connecting to tag");
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
boolean connected = isConnected();
|
||||
|
||||
while (true) {
|
||||
boolean newConnected = isConnected();
|
||||
if (newConnected != connected) {
|
||||
connected = newConnected;
|
||||
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
|
||||
|
||||
if (connected && !isRunning) {
|
||||
onCardConnected();
|
||||
} else {
|
||||
onCardDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
SystemClock.sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
private void onCardConnected() {
|
||||
isRunning = true;
|
||||
|
||||
onCardConnectedListener.onConnected(new CardChannel(isoDep));
|
||||
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
private void onCardDisconnected() {
|
||||
isRunning = false;
|
||||
isoDep = null;
|
||||
}
|
||||
|
||||
public void setOnCardConnectedListener(OnCardConnectedListener onConnectedListener) {
|
||||
onCardConnectedListener = onConnectedListener;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package im.status.hardwallet_lite_android.io;
|
||||
|
||||
public interface OnCardConnectedListener {
|
||||
void onConnected(CardChannel channel);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
/**
|
||||
* Parses the response from a SELECT command. If the card has not yet received the INIT command the isInitializedCard
|
||||
* will return false and only the getSecureChannelPubKey method will return a valid value.
|
||||
*/
|
||||
public class ApplicationInfo {
|
||||
private boolean initializedCard;
|
||||
private byte[] instanceUID;
|
||||
private byte[] secureChannelPubKey;
|
||||
private short appVersion;
|
||||
private byte freePairingSlots;
|
||||
private byte[] keyUID;
|
||||
private byte capabilities;
|
||||
|
||||
public static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
|
||||
public static final byte TLV_PUB_KEY = (byte) 0x80;
|
||||
public static final byte TLV_UID = (byte) 0x8F;
|
||||
public static final byte TLV_KEY_UID = (byte) 0x8E;
|
||||
public static final byte TLV_CAPABILITIES = (byte) 0x8D;
|
||||
|
||||
static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01;
|
||||
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
|
||||
static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04;
|
||||
static final byte CAPABILITY_NDEF = (byte) 0x08;
|
||||
|
||||
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF;
|
||||
|
||||
/**
|
||||
* Constructs an object by parsing the TLV data.
|
||||
*
|
||||
* @param tlvData the raw response data from the card
|
||||
* @throws IllegalArgumentException the TLV does not follow the allowed format
|
||||
*/
|
||||
public ApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
|
||||
int topTag = tlv.readTag();
|
||||
tlv.unreadLastTag();
|
||||
|
||||
if (topTag == TLV_PUB_KEY) {
|
||||
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
initializedCard = false;
|
||||
capabilities = CAPABILITY_CREDENTIALS_MANAGEMENT;
|
||||
|
||||
if (secureChannelPubKey.length > 0) {
|
||||
capabilities |= CAPABILITY_SECURE_CHANNEL;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
tlv.enterConstructed(TLV_APPLICATION_INFO_TEMPLATE);
|
||||
instanceUID = tlv.readPrimitive(TLV_UID);
|
||||
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
appVersion = (short) tlv.readInt();
|
||||
freePairingSlots = (byte) tlv.readInt();
|
||||
keyUID = tlv.readPrimitive(TLV_KEY_UID);
|
||||
|
||||
if (tlv.readTag() != TinyBERTLV.END_OF_TLV) {
|
||||
tlv.unreadLastTag();
|
||||
capabilities = tlv.readPrimitive(TLV_CAPABILITIES)[0];
|
||||
} else {
|
||||
capabilities = CAPABILITIES_ALL;
|
||||
}
|
||||
|
||||
initializedCard = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the card is initialized or not. If this method returns false, only the getSecureChannelPubKey method
|
||||
* will return a valid value.
|
||||
*
|
||||
* @return true if initialized, false otherwise
|
||||
*/
|
||||
public boolean isInitializedCard() {
|
||||
return initializedCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to discover if the card has a master key.
|
||||
*
|
||||
* @return true if the card has a master key, false otherwise
|
||||
*/
|
||||
public boolean hasMasterKey() {
|
||||
return keyUID.length != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance UID of the applet. This ID never changes for the lifetime of the applet.
|
||||
*
|
||||
* @return the instance UID
|
||||
*/
|
||||
public byte[] getInstanceUID() {
|
||||
return instanceUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public key to be used for secure channel opening. Usually handled internally by the KeycardCommandSet.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getSecureChannelPubKey() {
|
||||
return secureChannelPubKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The application version, encoded as a short. The msb is the major revision number and the lsb is the minor one.
|
||||
*
|
||||
* @return the application version
|
||||
*/
|
||||
public short getAppVersion() {
|
||||
return appVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A formatted application version.
|
||||
* @return the string representation of the application version
|
||||
*/
|
||||
public String getAppVersionString() {
|
||||
return (appVersion >> 8) + "." + (appVersion & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of remaining pairing slots. If zero is returned, no further pairing is possible.
|
||||
* @return the number of remaining pairing slots
|
||||
*/
|
||||
public byte getFreePairingSlots() {
|
||||
return freePairingSlots;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UID of the master key on this card. Changes every time a different master key is stored. It has zero length if
|
||||
* no key is on the card.
|
||||
*
|
||||
* @return the Key UID.
|
||||
*/
|
||||
public byte[] getKeyUID() {
|
||||
return keyUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the capability descriptor for the device.
|
||||
*
|
||||
* @return the capability descriptor for the device.
|
||||
*/
|
||||
public byte getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public boolean hasSecureChannelCapability() {
|
||||
return (capabilities & CAPABILITY_SECURE_CHANNEL) == CAPABILITY_SECURE_CHANNEL;
|
||||
}
|
||||
|
||||
public boolean hasKeyManagementCapability() {
|
||||
return (capabilities & CAPABILITY_KEY_MANAGEMENT) == CAPABILITY_KEY_MANAGEMENT;
|
||||
}
|
||||
|
||||
public boolean hasCredentialsManagementCapability() {
|
||||
return (capabilities & CAPABILITY_CREDENTIALS_MANAGEMENT) == CAPABILITY_CREDENTIALS_MANAGEMENT;
|
||||
}
|
||||
|
||||
public boolean hasNDEFCapability() {
|
||||
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
/**
|
||||
* Parses the result of a GET STATUS command retrieving application status.
|
||||
*/
|
||||
public class ApplicationStatus {
|
||||
private byte pinRetryCount;
|
||||
private byte pukRetryCount;
|
||||
private boolean hasMasterKey;
|
||||
|
||||
public static final byte TLV_APPLICATION_STATUS_TEMPLATE = (byte) 0xA3;
|
||||
|
||||
/**
|
||||
* Constructor from TLV data
|
||||
* @param tlvData the TLV data
|
||||
* @throws IllegalArgumentException if the TLV does not follow the expected format
|
||||
*/
|
||||
public ApplicationStatus(byte[] tlvData) throws IllegalArgumentException {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
tlv.enterConstructed(TLV_APPLICATION_STATUS_TEMPLATE);
|
||||
pinRetryCount = (byte) tlv.readInt();
|
||||
pukRetryCount = (byte) tlv.readInt();
|
||||
hasMasterKey = tlv.readBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* The available PIN retry count.
|
||||
* @return the available PIN retry count
|
||||
*/
|
||||
public byte getPINRetryCount() {
|
||||
return pinRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* The available PUK retry count.
|
||||
* @return the available PUK retry count
|
||||
*/
|
||||
public byte getPUKRetryCount() {
|
||||
return pukRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the card has a master key or not.
|
||||
*
|
||||
* @return whether the card has a master key or not.
|
||||
*/
|
||||
public boolean hasMasterKey() {
|
||||
return hasMasterKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.bouncycastle.crypto.digests.KeccakDigest;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a BIP32 keypair. This can be a master key or any other key in the path. Contains convenience method to
|
||||
* read and write formats the the card understands.
|
||||
*/
|
||||
public class BIP32KeyPair {
|
||||
private byte[] privateKey;
|
||||
private byte[] chainCode;
|
||||
private byte[] publicKey;
|
||||
|
||||
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
|
||||
static final byte TLV_PUB_KEY = (byte) 0x80;
|
||||
static final byte TLV_PRIV_KEY = (byte) 0x81;
|
||||
static final byte TLV_CHAIN_CODE = (byte) 0x82;
|
||||
|
||||
/**
|
||||
* Returns a BIP32 keypair from a BIP32 binary seed. It includes all components.
|
||||
*
|
||||
* @param binarySeed the binary seed
|
||||
* @return the BIP32 keypair
|
||||
*/
|
||||
public static BIP32KeyPair fromBinarySeed(byte[] binarySeed) {
|
||||
try {
|
||||
Mac hmacSHA512 = Mac.getInstance("HmacSHA512");
|
||||
SecretKeySpec keySpec = new SecretKeySpec("Bitcoin seed".getBytes(), "HmacSHA512");
|
||||
hmacSHA512.init(keySpec);
|
||||
byte[] mac = hmacSHA512.doFinal(binarySeed);
|
||||
return new BIP32KeyPair(Arrays.copyOf(mac, 32), Arrays.copyOfRange(mac, 32, 64), null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle correctly installed? ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a BIP32 keypair from a KEY TEMPLATE TLV. If the data is the output of the EXPORT KEY command, it will
|
||||
* never contain the chain code field and could contain only the public key.
|
||||
*
|
||||
* @param tlvData the TLV data
|
||||
* @return the BIP32 keypair
|
||||
*/
|
||||
public static BIP32KeyPair fromTLV(byte[] tlvData) {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
tlv.enterConstructed(TLV_KEY_TEMPLATE);
|
||||
|
||||
byte[] pubKey = null;
|
||||
byte[] privKey = null;
|
||||
byte[] chainCode = null;
|
||||
|
||||
int tag = tlv.readTag();
|
||||
|
||||
if (tag == TLV_PUB_KEY) {
|
||||
tlv.unreadLastTag();
|
||||
pubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
tag = tlv.readTag();
|
||||
}
|
||||
|
||||
if (tag == TLV_PRIV_KEY) {
|
||||
tlv.unreadLastTag();
|
||||
privKey = tlv.readPrimitive(TLV_PRIV_KEY);
|
||||
tag = tlv.readTag();
|
||||
|
||||
if (tag == TLV_CHAIN_CODE) {
|
||||
tlv.unreadLastTag();
|
||||
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
return new BIP32KeyPair(privKey, chainCode, pubKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level constructor. If the private key is not null, the public key can be omitted and it will be calculated
|
||||
* automatically. If the private key is null the chain code must be null and the public key must be not null.
|
||||
*
|
||||
* @param privateKey the private key
|
||||
* @param chainCode the chain code
|
||||
* @param publicKey the public key
|
||||
*/
|
||||
public BIP32KeyPair(byte[] privateKey, byte[] chainCode, byte[] publicKey) {
|
||||
if (privateKey == null && (chainCode != null || publicKey == null)) {
|
||||
throw new IllegalArgumentException("Private key can be null only if the public key is not null and the chain code is null");
|
||||
}
|
||||
|
||||
this.privateKey = privateKey;
|
||||
this.chainCode = chainCode;
|
||||
|
||||
if (publicKey != null) {
|
||||
this.publicKey = publicKey;
|
||||
} else {
|
||||
calculatePublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
private void calculatePublicKey() {
|
||||
BigInteger k = new BigInteger(1, this.privateKey);
|
||||
ECPoint pubKey = RecoverableSignature.CURVE.getG().multiply(k);
|
||||
this.publicKey = pubKey.getEncoded(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLV representation of this object.
|
||||
*
|
||||
* @return the TLV representation of this object.
|
||||
*/
|
||||
public byte[] toTLV() {
|
||||
return toTLV(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLV representation of this object, optionally omitting the public component.
|
||||
*
|
||||
* @return the TLV representation of this object.
|
||||
*/
|
||||
public byte[] toTLV(boolean includePublic) {
|
||||
int privLen = privateKey.length;
|
||||
int privOff = 0;
|
||||
|
||||
if(privateKey[0] == 0x00) {
|
||||
privOff++;
|
||||
privLen--;
|
||||
}
|
||||
|
||||
int off = 0;
|
||||
int totalLength = includePublic ? (publicKey.length + 2) : 0;
|
||||
totalLength += (privLen + 2);
|
||||
totalLength += isExtended() ? (chainCode.length + 2) : 0;
|
||||
|
||||
if (totalLength > 127) {
|
||||
totalLength += 3;
|
||||
} else {
|
||||
totalLength += 2;
|
||||
}
|
||||
|
||||
byte[] data = new byte[totalLength];
|
||||
data[off++] = TLV_KEY_TEMPLATE;
|
||||
|
||||
if (totalLength > 127) {
|
||||
data[off++] = (byte) 0x81;
|
||||
data[off++] = (byte) (totalLength - 3);
|
||||
} else {
|
||||
data[off++] = (byte) (totalLength - 2);
|
||||
}
|
||||
|
||||
if (includePublic) {
|
||||
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;
|
||||
|
||||
if (isExtended()) {
|
||||
data[off++] = (byte) TLV_CHAIN_CODE;
|
||||
data[off++] = (byte) chainCode.length;
|
||||
System.arraycopy(chainCode, 0, data, off, chainCode.length);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] toEthereumAddress() {
|
||||
KeccakDigest digest = new KeccakDigest(256);
|
||||
digest.update(publicKey, 1, (publicKey.length - 1));
|
||||
byte[] hash = new byte[32];
|
||||
digest.doFinal(hash, 0);
|
||||
return Arrays.copyOfRange(hash,12, hash.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private key. Might be null.
|
||||
*
|
||||
* @return the private key
|
||||
*/
|
||||
public byte[] getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chain code. Might be null.
|
||||
*
|
||||
* @return the chain code
|
||||
*/
|
||||
public byte[] getChainCode() {
|
||||
return chainCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key. Is never null.
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if only the public key is contained, false otherwise.
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean isPublicOnly() {
|
||||
return privateKey == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the chain code is contained, false otherwise.
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean isExtended() {
|
||||
return chainCode != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.io.WrongPINException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Class helping with the card duplication process. Depending on the client's role, only some of the methods are relevant.
|
||||
*/
|
||||
public class CardDuplicator {
|
||||
private byte[] secret;
|
||||
private KeycardCommandSet cmdSet;
|
||||
private DuplicatorCallback cb;
|
||||
|
||||
private HashSet<byte[]> startedDuplication;
|
||||
private HashSet<byte[]> addedEntropy;
|
||||
private HashSet<byte[]> finishedDuplication;
|
||||
|
||||
/**
|
||||
* Creates a CardDuplicator object. Regardless of the role of the client, this object must be kept and used for the
|
||||
* entire duplication session. It cannot be reused for multiple sessions.
|
||||
*
|
||||
* @param cmdSet the CommandSet to use
|
||||
* @param cb the callback object for backups. This is needed only on the client performing steps requiring pairing
|
||||
* and authentication. Clients which only add entropy should pass null
|
||||
*/
|
||||
public CardDuplicator(KeycardCommandSet cmdSet, DuplicatorCallback cb) {
|
||||
this.cmdSet = cmdSet;
|
||||
this.cb = cb;
|
||||
this.startedDuplication = new HashSet<>();
|
||||
this.addedEntropy = new HashSet<>();
|
||||
this.finishedDuplication = new HashSet<>();
|
||||
|
||||
this.secret = new byte[32];
|
||||
SecureRandom random = new SecureRandom();
|
||||
random.nextBytes(this.secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CardDuplicator object. Only suitable for clients performing the role of adding entropy.
|
||||
*
|
||||
* @param channel the APDU channel
|
||||
*/
|
||||
public CardDuplicator(CardChannel channel) {
|
||||
this(new KeycardCommandSet(channel), null);
|
||||
}
|
||||
|
||||
private ApplicationInfo selectAndCheck(HashSet<byte[]> processed) throws APDUException, IOException {
|
||||
ApplicationInfo appInfo = new ApplicationInfo(cmdSet.select().checkOK().getData());
|
||||
|
||||
if (!processed.add(appInfo.getInstanceUID())) {
|
||||
throw new IllegalStateException("The requested action has been already performed on this card");
|
||||
}
|
||||
|
||||
return appInfo;
|
||||
}
|
||||
|
||||
private void preamble(HashSet<byte[]> processed) throws IOException, APDUException {
|
||||
ApplicationInfo appInfo = selectAndCheck(processed);
|
||||
|
||||
Pairing pairing = cb.getPairing(appInfo);
|
||||
|
||||
if (pairing == null) {
|
||||
throw new APDUException("The given card is not paired");
|
||||
}
|
||||
|
||||
cmdSet.setPairing(pairing);
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
ApplicationStatus appStatus = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
|
||||
int remainingAttempts = appStatus.getPINRetryCount();
|
||||
|
||||
while(remainingAttempts > 0) {
|
||||
try {
|
||||
cmdSet.verifyPIN(cb.getPIN(appInfo, remainingAttempts)).checkAuthOK();
|
||||
break;
|
||||
} catch(WrongPINException e) {
|
||||
remainingAttempts = e.getRetryAttempts();
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingAttempts <= 0) {
|
||||
throw new APDUException("Card blocked");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts duplication session. Must be used on all cards taking part of in the duplication process.
|
||||
*
|
||||
* @param clientCount the number of clients which will be adding entropy for the key, including this one
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unexpected card response
|
||||
* @throws IllegalStateException this card has already been used
|
||||
*/
|
||||
public void startDuplication(int clientCount) throws IOException, APDUException, IllegalStateException {
|
||||
preamble(startedDuplication);
|
||||
cmdSet.duplicateKeyStart(clientCount, secret).checkOK();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports key. Must be used on the card designated as the source for the duplication.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unexpected card response
|
||||
*/
|
||||
public byte[] exportKey() throws IOException, APDUException, IllegalStateException {
|
||||
preamble(finishedDuplication);
|
||||
return cmdSet.duplicateKeyExport().checkOK().getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports key. Must be used on all cards designated as the target for the duplication.
|
||||
*
|
||||
* @param key the key to import
|
||||
* @return the key UID
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unexpected card response
|
||||
* @throws IllegalStateException this card has already been used
|
||||
*/
|
||||
public byte[] importKey(byte[] key) throws IOException, APDUException, IllegalStateException {
|
||||
preamble(finishedDuplication);
|
||||
return cmdSet.duplicateKeyImport(key).checkOK().getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entropy. Must be used on all cards taking part in the backup process. Each client taking part must use this
|
||||
* exactly once, except for the client which started the backup.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unexpected card response
|
||||
* @throws IllegalStateException this card has already been used
|
||||
*/
|
||||
public void addEntropy() throws IOException, APDUException, IllegalStateException {
|
||||
selectAndCheck(addedEntropy);
|
||||
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
/**
|
||||
* Callback interface for duplication procedure.
|
||||
*/
|
||||
public interface DuplicatorCallback {
|
||||
/**
|
||||
* Must return the pairing for the current card, represented by the applicationInfo parameter. If no pairing
|
||||
* could be found, null must be returned.
|
||||
*
|
||||
* @param applicationInfo the application info template of the currently inserted card
|
||||
* @return the pairing info or null
|
||||
*/
|
||||
Pairing getPairing(ApplicationInfo applicationInfo);
|
||||
|
||||
/**
|
||||
* Must return the PIN for the current card. This method can prompt the user or return a cached value.
|
||||
*
|
||||
* @param applicationInfo the application info template of the currently inserted card
|
||||
* @param remainingAttempts the number of remaining PIN attempts
|
||||
* @return the PIN
|
||||
*/
|
||||
String getPIN(ApplicationInfo applicationInfo, int remainingAttempts);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Identifiers {
|
||||
public static final byte[] PACKAGE_AID = Hex.decode("A0000008040001");
|
||||
|
||||
public static final byte[] KEYCARD_AID = Hex.decode("A000000804000101");
|
||||
public static final int KEYCARD_DEFAULT_INSTANCE_IDX = 1;
|
||||
|
||||
public static final byte[] NDEF_AID = Hex.decode("A000000804000102");
|
||||
public static final byte[] NDEF_INSTANCE_AID = Hex.decode("D2760000850101");
|
||||
|
||||
/**
|
||||
* Gets the instance AID of the default instance of the Keycard applet.
|
||||
*
|
||||
* @return the instance AID of the Keycard applet
|
||||
*/
|
||||
public static byte[] getKeycardInstanceAID() {
|
||||
return getKeycardInstanceAID(KEYCARD_DEFAULT_INSTANCE_IDX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the instance AID of the Keycard applet with the given index. Since multiple instances of the Keycard applet
|
||||
* could be installed in parallel, this method allows selecting a specific instance. The index is between 01 and ff
|
||||
*
|
||||
* @return the instance AID of the Keycard applet
|
||||
*/
|
||||
public static byte[] getKeycardInstanceAID(int instanceIdx) {
|
||||
if (instanceIdx < 0x01 || instanceIdx > 0xff) {
|
||||
throw new IllegalArgumentException("The instance index must be between 1 and 255");
|
||||
}
|
||||
|
||||
byte[] instanceAID = Arrays.copyOf(KEYCARD_AID, KEYCARD_AID.length + 1);
|
||||
instanceAID[KEYCARD_AID.length] = (byte) instanceIdx;
|
||||
return instanceAID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* Keypath object to be used with the KeycardCommandSet
|
||||
*/
|
||||
public class KeyPath {
|
||||
private int source;
|
||||
private byte[] data;
|
||||
|
||||
/**
|
||||
* Parses a keypath into a byte array and source parameter to be used with the KeycardCommandSet object.
|
||||
*
|
||||
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
|
||||
*
|
||||
* The first component can be either "m", indicating the master key, "..", indicating the parent of the current key,
|
||||
* or "." indicating the current key. It can also be omitted, in which case it is considered the same as being ".".
|
||||
*
|
||||
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
|
||||
* which indicates an hardened key.
|
||||
*
|
||||
* An example of a valid path is "m/44'/0'/0'/0/0"
|
||||
*
|
||||
*
|
||||
* @param keypath the keypath as a string
|
||||
*/
|
||||
public KeyPath(String keypath) {
|
||||
StringTokenizer tokenizer = new StringTokenizer(keypath, "/");
|
||||
|
||||
String sourceOrFirstElement = tokenizer.nextToken();
|
||||
|
||||
switch(sourceOrFirstElement) {
|
||||
case "m":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_MASTER;
|
||||
break;
|
||||
case "..":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_PARENT;
|
||||
break;
|
||||
case ".":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
|
||||
break;
|
||||
default:
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
|
||||
tokenizer = new StringTokenizer(keypath, "/"); // rewind
|
||||
break;
|
||||
}
|
||||
|
||||
int componentCount = tokenizer.countTokens();
|
||||
if (componentCount > 10) {
|
||||
throw new IllegalArgumentException("Too many components");
|
||||
}
|
||||
|
||||
data = new byte[4 * componentCount];
|
||||
|
||||
for (int i = 0; i < componentCount; i++) {
|
||||
long component = parseComponent(tokenizer.nextToken());
|
||||
writeComponent(component, i);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPath(byte[] data, int source) {
|
||||
this.data = data;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public KeyPath(byte[] data) {
|
||||
this(data, KeycardCommandSet.DERIVE_P1_SOURCE_MASTER);
|
||||
}
|
||||
|
||||
private long parseComponent(String num) {
|
||||
long sign;
|
||||
|
||||
if (num.endsWith("'")) {
|
||||
sign = 0x80000000L;
|
||||
num = num.substring(0, (num.length() - 1));
|
||||
} else {
|
||||
sign = 0L;
|
||||
}
|
||||
|
||||
if (num.startsWith("+") || num.startsWith("-")) {
|
||||
throw new NumberFormatException("No sign allowed");
|
||||
}
|
||||
return (sign | Long.parseLong(num));
|
||||
}
|
||||
|
||||
private void writeComponent(long component, int i) {
|
||||
int off = (i*4);
|
||||
data[off] = (byte)((component >> 24) & 0xff);
|
||||
data[off + 1] = (byte)((component >> 16) & 0xff);
|
||||
data[off + 2] = (byte)((component >> 8) & 0xff);
|
||||
data[off + 3] = (byte)(component & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* The source of the derive command.
|
||||
*
|
||||
* @return the source of the derive command
|
||||
*/
|
||||
public int getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* The byte encoded key path.
|
||||
*
|
||||
* @return byte encoded key path
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
switch(source) {
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_MASTER:
|
||||
sb.append('m');
|
||||
break;
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_PARENT:
|
||||
sb.append("..");
|
||||
break;
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT:
|
||||
sb.append('.');
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.data.length; i += 4) {
|
||||
sb.append('/');
|
||||
appendComponent(sb, i);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendComponent(StringBuffer sb, int i) {
|
||||
int num = ((this.data[i] & 0x7f) << 24) | ((this.data[i+1] & 0xff) << 16) | ((this.data[i+2] & 0xff) << 8) | (this.data[i+3] & 0xff);
|
||||
sb.append(num);
|
||||
|
||||
if ((this.data[i] & 0x80) == 0x80) {
|
||||
sb.append('\'');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
package im.status.hardwallet_lite_android.wallet;
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import im.status.hardwallet_lite_android.io.APDUCommand;
|
||||
import im.status.hardwallet_lite_android.io.APDUException;
|
||||
import im.status.hardwallet_lite_android.io.APDUResponse;
|
||||
import im.status.hardwallet_lite_android.io.CardChannel;
|
||||
import org.spongycastle.jce.interfaces.ECPrivateKey;
|
||||
import org.spongycastle.jce.interfaces.ECPublicKey;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import im.status.keycard.io.*;
|
||||
import org.bouncycastle.jce.interfaces.ECPrivateKey;
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@@ -21,9 +16,10 @@ import java.util.Arrays;
|
||||
* 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 {
|
||||
public class KeycardCommandSet {
|
||||
static final byte INS_INIT = (byte) 0xFE;
|
||||
static final byte INS_GET_STATUS = (byte) 0xF2;
|
||||
static final byte INS_SET_NDEF = (byte) 0xF3;
|
||||
static final byte INS_VERIFY_PIN = (byte) 0x20;
|
||||
static final byte INS_CHANGE_PIN = (byte) 0x21;
|
||||
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
|
||||
@@ -31,65 +27,124 @@ public class WalletAppletCommandSet {
|
||||
static final byte INS_DERIVE_KEY = (byte) 0xD1;
|
||||
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
|
||||
static final byte INS_REMOVE_KEY = (byte) 0xD3;
|
||||
static final byte INS_GENERATE_KEY = (byte) 0xD4;
|
||||
static final byte INS_DUPLICATE_KEY = (byte) 0xD5;
|
||||
static final byte INS_SIGN = (byte) 0xC0;
|
||||
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
|
||||
static final byte INS_EXPORT_KEY = (byte) 0xC2;
|
||||
|
||||
public static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
|
||||
public static final byte CHANGE_PIN_P1_PUK = 0x01;
|
||||
public static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02;
|
||||
|
||||
public static final byte GET_STATUS_P1_APPLICATION = 0x00;
|
||||
public static final byte GET_STATUS_P1_KEY_PATH = 0x01;
|
||||
|
||||
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;
|
||||
public static final byte LOAD_KEY_P1_EC = 0x01;
|
||||
public static final byte LOAD_KEY_P1_EXT_EC = 0x02;
|
||||
public static final byte LOAD_KEY_P1_SEED = 0x03;
|
||||
|
||||
static final byte DERIVE_P1_ASSISTED_MASK = 0x01;
|
||||
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
|
||||
public static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
|
||||
public static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
|
||||
public static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
|
||||
|
||||
static final byte DERIVE_P2_KEY_PATH = 0x00;
|
||||
static final byte DERIVE_P2_PUBLIC_KEY = 0x01;
|
||||
static final byte DUPLICATE_KEY_P1_START = 0x00;
|
||||
static final byte DUPLICATE_KEY_P1_ADD_ENTROPY = 0x01;
|
||||
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
|
||||
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
|
||||
|
||||
public static final int GENERATE_MNEMONIC_12_WORDS = 0x04;
|
||||
public static final int GENERATE_MNEMONIC_15_WORDS = 0x05;
|
||||
public static final int GENERATE_MNEMONIC_18_WORDS = 0x06;
|
||||
public static final int GENERATE_MNEMONIC_21_WORDS = 0x07;
|
||||
public static final int GENERATE_MNEMONIC_24_WORDS = 0x08;
|
||||
|
||||
static final byte EXPORT_KEY_P1_CURRENT = 0x00;
|
||||
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
|
||||
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
|
||||
|
||||
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;
|
||||
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
|
||||
|
||||
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;
|
||||
private ApplicationInfo info;
|
||||
|
||||
public WalletAppletCommandSet(CardChannel apduChannel) {
|
||||
/**
|
||||
* Creates a KeycardCommandSet using the given APDU Channel
|
||||
* @param apduChannel APDU channel
|
||||
*/
|
||||
public KeycardCommandSet(CardChannel apduChannel) {
|
||||
this.apduChannel = apduChannel;
|
||||
this.secureChannel = new SecureChannelSession();
|
||||
}
|
||||
|
||||
public void setSecureChannel(SecureChannelSession secureChannel) {
|
||||
/**
|
||||
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
|
||||
* command has been sent using this command set.
|
||||
*
|
||||
* @return the application info object
|
||||
*/
|
||||
public ApplicationInfo getApplicationInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SecureChannel object
|
||||
* @param secureChannel secure channel
|
||||
*/
|
||||
protected 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
|
||||
* Returns the current pairing data.
|
||||
*/
|
||||
public Pairing getPairing() {
|
||||
return secureChannel.getPairing();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sets the pairing data.
|
||||
* @param pairing data from an existing pairing
|
||||
*/
|
||||
public void setPairing(Pairing pairing) {
|
||||
secureChannel.setPairing(pairing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the default instance of the Keycard 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 {
|
||||
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES);
|
||||
return select(Identifiers.KEYCARD_DEFAULT_INSTANCE_IDX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a Keycard instance. 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.
|
||||
*
|
||||
* @param instanceIdx the instance index
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse select(int instanceIdx) throws IOException {
|
||||
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID(instanceIdx));
|
||||
APDUResponse resp = apduChannel.send(selectApplet);
|
||||
|
||||
|
||||
if (resp.getSw() == 0x9000) {
|
||||
byte[] keyData = extractPublicKeyFromSelect(resp.getData());
|
||||
this.secureChannel = new SecureChannelSession(keyData);
|
||||
info = new ApplicationInfo(resp.getData());
|
||||
|
||||
if (info.hasSecureChannelCapability()) {
|
||||
this.secureChannel.generateSecret(info.getSecureChannelPubKey());
|
||||
this.secureChannel.reset();
|
||||
}
|
||||
}
|
||||
|
||||
return resp;
|
||||
@@ -98,7 +153,6 @@ public class WalletAppletCommandSet {
|
||||
/**
|
||||
* 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 {
|
||||
@@ -111,17 +165,28 @@ public class WalletAppletCommandSet {
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void autoPair(String pairingPassword) throws IOException {
|
||||
byte[] secret = pairingPasswordToSecret(pairingPassword);
|
||||
|
||||
secureChannel.autoPair(apduChannel, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a pairing password to a binary pairing secret.
|
||||
*
|
||||
* @param pairingPassword the pairing password
|
||||
* @return the pairing secret
|
||||
*/
|
||||
public byte[] pairingPasswordToSecret(String pairingPassword) {
|
||||
SecretKey key;
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Status Hardware Wallet Lite".getBytes(), 50000, 32 * 8);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", "BC");
|
||||
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), apduChannel.pairingPasswordPBKDF2IterationCount(), 32 * 8);
|
||||
key = skf.generateSecret(spec);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is Bouncycastle correctly initialized?");
|
||||
}
|
||||
|
||||
secureChannel.autoPair(apduChannel, key.getEncoded());
|
||||
return key.getEncoded();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,36 +263,22 @@ public class WalletAppletCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key
|
||||
* derivation support.
|
||||
* Sends a SET NDEF APDU.
|
||||
*
|
||||
* @return whether public key derivation is supported or not
|
||||
* @param ndef the data field of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public boolean getPublicKeyDerivationSupport() throws IOException {
|
||||
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
|
||||
byte[] data = resp.getData();
|
||||
return data[data.length - 1] != 0x00;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization
|
||||
* status
|
||||
*
|
||||
* @return whether public key derivation is supported or not
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public boolean getKeyInitializationStatus() throws IOException {
|
||||
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
|
||||
byte[] data = resp.getData();
|
||||
return data[data.length - 4] != 0x00;
|
||||
public APDUResponse setNDEF(byte[] ndef) throws IOException {
|
||||
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
|
||||
return secureChannel.transmit(apduChannel, setNDEF);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param pin the PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
@@ -236,6 +287,40 @@ public class WalletAppletCommandSet {
|
||||
return secureChannel.transmit(apduChannel, verifyPIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the user PIN.
|
||||
*
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePIN(String pin) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_USER_PIN, pin.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the PUK.
|
||||
*
|
||||
* @param puk the new PUK
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePUK(String puk) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_PUK, puk.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the pairing password. This does not break existing pairings, but new pairings
|
||||
* will be made using the new password.
|
||||
*
|
||||
* @param pairingPassword the new pairing password
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePairingPassword(String pairingPassword) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_PAIRING_SECRET, pairingPasswordToSecret(pairingPassword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
@@ -276,31 +361,16 @@ public class WalletAppletCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sends a LOAD KEY APDU. The given seed is sent as-is 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
|
||||
* @param seed the binary seed
|
||||
* @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);
|
||||
public APDUResponse loadKey(byte[] seed) throws IOException {
|
||||
return loadKey(seed, LOAD_KEY_P1_SEED);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,10 +397,10 @@ public class WalletAppletCommandSet {
|
||||
* @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[] publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
|
||||
byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray();
|
||||
|
||||
return loadKey(publicKey, privateKey, chainCode);
|
||||
return loadKey(new BIP32KeyPair(privateKey, chainCode, publicKey), omitPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,59 +415,23 @@ public class WalletAppletCommandSet {
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
|
||||
int privLen = privateKey.length;
|
||||
int privOff = 0;
|
||||
return loadKey(new BIP32KeyPair(privateKey, chainCode, publicKey), publicKey == null);
|
||||
}
|
||||
|
||||
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;
|
||||
public APDUResponse loadKey(BIP32KeyPair keyPair) throws IOException {
|
||||
return loadKey(keyPair, false);
|
||||
}
|
||||
|
||||
public APDUResponse loadKey(BIP32KeyPair keyPair, boolean omitPublic) throws IOException {
|
||||
byte p1;
|
||||
|
||||
if (chainCode != null) {
|
||||
if (keyPair.isExtended()) {
|
||||
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);
|
||||
return loadKey(keyPair.toTLV(!omitPublic), p1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,53 +471,113 @@ public class WalletAppletCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sends a GENERATE KEY APDU.
|
||||
*
|
||||
* @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);
|
||||
public APDUResponse generateKey() throws IOException {
|
||||
APDUCommand generateKey = secureChannel.protectedCommand(0x80, INS_GENERATE_KEY, 0, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, generateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 00, P2 to the entropy count and the data is the first entropy piece.
|
||||
* This starts a duplication session. Requires an open Secure Channel and authenticated PIN.
|
||||
*
|
||||
* @param entropyCount the number of entropy pieces to expect, including the one in this APDU
|
||||
* @param firstEntropy a random 32-byte number
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyStart(int entropyCount, byte[] firstEntropy) throws IOException {
|
||||
APDUCommand duplicateKeyStart = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_START, entropyCount, firstEntropy);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 01 and the data is the entropy. This adds entropy and does not require
|
||||
* a Secure Channel or authenticated PIN.
|
||||
*
|
||||
* @param entropy a random 32-byte number
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyAddEntropy(byte[] entropy) throws IOException {
|
||||
APDUCommand duplicateKeyAddEntropy = new APDUCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_ADD_ENTROPY, 0, secureChannel.oneShotEncrypt(entropy));
|
||||
return apduChannel.send(duplicateKeyAddEntropy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 02. This exports the encrypted master key including chaining code.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyExport() throws IOException {
|
||||
APDUCommand duplicateKeyExport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_EXPORT, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyExport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 03. This imports an encrypted master key including chaining code. The
|
||||
* response data contains the key UID of the imported key.
|
||||
*
|
||||
* @param key the key, exported from another card in the same duplication session.
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyImport(byte[] key) throws IOException {
|
||||
APDUCommand duplicateKeyImport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_IMPORT, 0, key);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyImport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
|
||||
*
|
||||
* @param data the data to sign
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse sign(byte[] data) throws IOException {
|
||||
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, 0x00, 0x00, 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.
|
||||
* Sends a DERIVE KEY APDU with the given key path.
|
||||
*
|
||||
* @param keypath the string key path
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deriveKey(String keypath) throws IOException {
|
||||
KeyPath path = new KeyPath(keypath);
|
||||
return deriveKey(path.getData(), path.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
|
||||
* starts from the master key.
|
||||
*
|
||||
* @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);
|
||||
return deriveKey(data, DERIVE_P1_SOURCE_MASTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The source parameter is used as P1.
|
||||
*
|
||||
* @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);
|
||||
public APDUResponse deriveKey(byte[] data, int source) throws IOException {
|
||||
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, source, 0x00, data);
|
||||
return secureChannel.transmit(apduChannel, deriveKey);
|
||||
}
|
||||
|
||||
@@ -500,19 +594,72 @@ public class WalletAppletCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself
|
||||
* Sends an EXPORT KEY APDU to export the current key.
|
||||
*
|
||||
* @param keyPathIndex the P1 parameter
|
||||
* @param publicOnly exports only the public key
|
||||
* @return the raw card reponse
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportCurrentKey(boolean publicOnly) throws IOException {
|
||||
return exportKey(EXPORT_KEY_P1_CURRENT, publicOnly, new byte[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param publicOnly the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(byte keyPathIndex, boolean publicOnly) throws IOException {
|
||||
public APDUResponse exportKey(String keyPath, boolean makeCurrent, boolean publicOnly) throws IOException {
|
||||
KeyPath path = new KeyPath(keyPath);
|
||||
return exportKey(path.getData(), path.getSource(), makeCurrent, publicOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param publicOnly the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, boolean publicOnly) throws IOException {
|
||||
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
|
||||
return exportKey(p1, publicOnly, keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
|
||||
*
|
||||
* @param derivationOptions the P1 parameter
|
||||
* @param publicOnly the P2 parameter
|
||||
* @param keypath the data parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(int derivationOptions, boolean publicOnly, byte[] keypath) 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]);
|
||||
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, derivationOptions, p2, keypath);
|
||||
return secureChannel.transmit(apduChannel, exportKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param pairingPassword pairing password
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
|
||||
return this.init(pin, puk, pairingPasswordToSecret(pairingPassword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
@@ -529,14 +676,4 @@ public class WalletAppletCommandSet {
|
||||
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
|
||||
return apduChannel.send(init);
|
||||
}
|
||||
|
||||
private byte[] extractPublicKeyFromSelect(byte[] select) {
|
||||
if (select[0] == TLV_APPLICATION_INFO_TEMPLATE) {
|
||||
return Arrays.copyOfRange(select, 22, 22 + select[21]);
|
||||
} else if (select[0] == TLV_PUB_KEY) {
|
||||
return Arrays.copyOfRange(select, 2, select.length);
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected card response");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class Mnemonic {
|
||||
private final static int WORDLIST_SIZE = 2048;
|
||||
|
||||
private short[] indexes;
|
||||
private String[] wordlist;
|
||||
|
||||
/**
|
||||
* Constructs a Mnemonic object from the response of the GENERATE MNEMONIC APDU
|
||||
*
|
||||
* @param data the card response
|
||||
*/
|
||||
public Mnemonic(byte[] data) {
|
||||
this.indexes = new short[data.length/2];
|
||||
|
||||
for (int i = 0; i < this.indexes.length; i++) {
|
||||
this.indexes[i] = (short) (((data[i * 2] & 0xff) << 8) | (data[(i * 2) + 1] & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wordlist, which must be a list of 2048 words.
|
||||
*
|
||||
* @param wordlist
|
||||
*/
|
||||
public void setWordlist(String[] wordlist) {
|
||||
if (wordlist.length != WORDLIST_SIZE) {
|
||||
throw new IllegalArgumentException("The list must contain exactly 2048 entries");
|
||||
}
|
||||
|
||||
this.wordlist = wordlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the official BIP39 english wordlist from GitHub.
|
||||
*
|
||||
* @throws IOException network error
|
||||
*/
|
||||
public void fetchBIP39EnglishWordlist() throws IOException {
|
||||
URL remoteList = new URL("https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt");
|
||||
Scanner scanner = new Scanner(remoteList.openStream());
|
||||
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
|
||||
while(scanner.hasNextLine()) {
|
||||
list.add(scanner.nextLine());
|
||||
}
|
||||
|
||||
scanner.close();
|
||||
|
||||
if (list.size() != WORDLIST_SIZE) {
|
||||
throw new IllegalArgumentException("The list must contain exactly 2048 entries");
|
||||
}
|
||||
|
||||
this.wordlist = new String[WORDLIST_SIZE];
|
||||
list.toArray(this.wordlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the indexes of all words of the mnemonic.
|
||||
* @return indexes
|
||||
*/
|
||||
public short[] getIndexes() {
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The words of the mnemonic phrase. Requires the wordlist to be non-null.
|
||||
*
|
||||
* @return the array of words
|
||||
*/
|
||||
public String[] getWords() {
|
||||
if (this.wordlist == null) {
|
||||
throw new IllegalStateException("The wordlist must be set first");
|
||||
}
|
||||
|
||||
String[] words = new String[this.indexes.length];
|
||||
|
||||
for (int i = 0; i < this.indexes.length; i++) {
|
||||
words[i] = this.wordlist[this.indexes[i]];
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* The representation of this object as a mnemonic phrase. Requires the wordlist to be non-null.
|
||||
* @return the mnemonic phrase
|
||||
*/
|
||||
public String toMnemonicPhrase() {
|
||||
return join(" ", getWords());
|
||||
}
|
||||
|
||||
/**
|
||||
* The binary seed representation of this object, with no password.
|
||||
*
|
||||
* @return the binary seed
|
||||
*/
|
||||
public byte[] toBinarySeed() {
|
||||
return toBinarySeed("");
|
||||
}
|
||||
|
||||
/**
|
||||
* The binary seed representation of this object, with a password.
|
||||
*
|
||||
* @param password can be an empty string but not null
|
||||
* @return the binary seed
|
||||
*/
|
||||
public byte[] toBinarySeed(String password) {
|
||||
return toBinarySeed(toMnemonicPhrase(), password);
|
||||
}
|
||||
|
||||
/**
|
||||
* The full master key, generated from this mnemonic.
|
||||
*
|
||||
*/
|
||||
public BIP32KeyPair toBIP32KeyPair() {
|
||||
return toBIP32KeyPair("");
|
||||
}
|
||||
|
||||
/**
|
||||
* The full master key, generated from this mnemonic with a password.
|
||||
*
|
||||
* @param password can be an empty string but not null
|
||||
* @return the binary seed
|
||||
*/
|
||||
public BIP32KeyPair toBIP32KeyPair(String password) {
|
||||
return BIP32KeyPair.fromBinarySeed(toBinarySeed(password));
|
||||
}
|
||||
|
||||
public static byte[] toBinarySeed(String mnemonicPhrase, String password) {
|
||||
SecretKey key;
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512", "BC");
|
||||
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
|
||||
key = skf.generateSecret(spec);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is Bouncycastle correctly initialized?");
|
||||
}
|
||||
|
||||
return key.getEncoded();
|
||||
}
|
||||
|
||||
/**
|
||||
* String join. Used instead of Android TextUtils.join or Java 8 String.join method for compatibility reasons.
|
||||
*
|
||||
* @param list the list of words
|
||||
* @param conjunction the conjunction
|
||||
*
|
||||
* @return the joined string
|
||||
*/
|
||||
private String join(String conjunction, String[] list) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (String item : list) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(conjunction);
|
||||
}
|
||||
|
||||
sb.append(item);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Stores pairing information.
|
||||
*/
|
||||
public class Pairing {
|
||||
private byte[] pairingKey;
|
||||
private byte pairingIndex;
|
||||
|
||||
/**
|
||||
* Constructor. The pairingKey and pairingIndex are those generated at the end of a successful pairing.
|
||||
* @param pairingKey the pairing key
|
||||
* @param pairingIndex the pairing index
|
||||
*/
|
||||
public Pairing(byte[] pairingKey, byte pairingIndex) {
|
||||
this.pairingKey = pairingKey;
|
||||
this.pairingIndex = pairingIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Initializes from a byte array previously generated from the toByteArray method
|
||||
* @param fromByteArray the result of a previous toByteArray invocation
|
||||
*/
|
||||
public Pairing(byte[] fromByteArray) {
|
||||
pairingIndex = fromByteArray[0];
|
||||
pairingKey = Arrays.copyOfRange(fromByteArray, 1, fromByteArray.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Initializes from a String previously generated from the toBase64 method
|
||||
* @param base64 the result of a previous toBase64 invocation
|
||||
*/
|
||||
public Pairing(String base64) {
|
||||
this(Base64.decode(base64));
|
||||
}
|
||||
|
||||
public byte[] getPairingKey() {
|
||||
return pairingKey;
|
||||
}
|
||||
|
||||
public byte getPairingIndex() {
|
||||
return pairingIndex;
|
||||
}
|
||||
|
||||
public byte[] toByteArray() {
|
||||
byte[] res = new byte[pairingKey.length + 1];
|
||||
res[0] = pairingIndex;
|
||||
System.arraycopy(pairingKey, 0, res, 1, pairingKey.length);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
return Base64.toBase64String(toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||
import org.bouncycastle.math.ec.ECAlgorithms;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.bouncycastle.math.ec.FixedPointUtil;
|
||||
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Signature with recoverable public key.
|
||||
*/
|
||||
public class RecoverableSignature {
|
||||
private byte[] publicKey;
|
||||
private int recId;
|
||||
private byte[] r;
|
||||
private byte[] s;
|
||||
|
||||
public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
|
||||
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
|
||||
|
||||
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
|
||||
static final ECDomainParameters CURVE;
|
||||
|
||||
static {
|
||||
FixedPointUtil.precompute(CURVE_PARAMS.getG());
|
||||
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a signature from the card and calculates the recovery ID.
|
||||
*
|
||||
* @param hash the message being signed
|
||||
* @param tlvData the signature as returned from the card
|
||||
*/
|
||||
public RecoverableSignature(byte[] hash, byte[] tlvData) {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
|
||||
publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
|
||||
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
|
||||
r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
|
||||
s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
|
||||
|
||||
recId = -1;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
byte[] candidate = recoverFromSignature(i, new BigInteger(1, hash), new BigInteger(1, r), new BigInteger(1, s));
|
||||
|
||||
if (Arrays.equals(candidate, publicKey)) {
|
||||
recId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (recId == -1) {
|
||||
throw new IllegalArgumentException("Unrecoverable signature, cannot find recId");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] toUInt(byte[] signedInt) {
|
||||
if (signedInt[0] == 0) {
|
||||
return Arrays.copyOfRange(signedInt, 1, signedInt.length);
|
||||
} else {
|
||||
return signedInt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public key associated to this signature.
|
||||
*
|
||||
* @return the public key associated to this signature
|
||||
*/
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The recovery ID
|
||||
*
|
||||
* @return recovery ID
|
||||
*/
|
||||
public int getRecId() {
|
||||
return recId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The R value.
|
||||
*
|
||||
* @return r
|
||||
*/
|
||||
public byte[] getR() {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* The S value
|
||||
* @return s
|
||||
*/
|
||||
public byte[] getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) {
|
||||
BigInteger n = CURVE.getN();
|
||||
BigInteger i = BigInteger.valueOf((long) recId / 2);
|
||||
BigInteger x = r.add(i.multiply(n));
|
||||
BigInteger prime = SecP256K1Curve.q;
|
||||
|
||||
if (x.compareTo(prime) >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ECPoint R = decompressKey(x, (recId & 1) == 1);
|
||||
|
||||
if (!R.multiply(n).isInfinity()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
|
||||
BigInteger rInv = r.modInverse(n);
|
||||
BigInteger srInv = rInv.multiply(s).mod(n);
|
||||
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
|
||||
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
|
||||
return q.getEncoded(false);
|
||||
}
|
||||
|
||||
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
|
||||
X9IntegerConverter x9 = new X9IntegerConverter();
|
||||
byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve()));
|
||||
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
|
||||
return CURVE.getCurve().decodePoint(compEnc);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package im.status.hardwallet_lite_android.wallet;
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import im.status.hardwallet_lite_android.io.APDUCommand;
|
||||
import im.status.hardwallet_lite_android.io.APDUException;
|
||||
import im.status.hardwallet_lite_android.io.APDUResponse;
|
||||
import im.status.hardwallet_lite_android.io.CardChannel;
|
||||
import 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 im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.bouncycastle.jce.spec.ECPublicKeySpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
@@ -42,9 +42,8 @@ public class SecureChannelSession {
|
||||
|
||||
private byte[] secret;
|
||||
private byte[] publicKey;
|
||||
private byte[] pairingKey;
|
||||
private byte[] iv;
|
||||
private byte pairingIndex;
|
||||
private Pairing pairing;
|
||||
private Cipher sessionCipher;
|
||||
private CBCBlockCipherMac sessionMac;
|
||||
private SecretKeySpec sessionEncKey;
|
||||
@@ -53,32 +52,33 @@ public class SecureChannelSession {
|
||||
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
|
||||
* Constructs a SecureChannel session on the client.
|
||||
*/
|
||||
public SecureChannelSession(byte[] keyData) {
|
||||
public SecureChannelSession() {
|
||||
random = new SecureRandom();
|
||||
generateSecret(keyData);
|
||||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a pairing secret. This should be called before 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 void generateSecret(byte[] keyData) {
|
||||
try {
|
||||
random = new SecureRandom();
|
||||
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
||||
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
|
||||
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
|
||||
g.initialize(ecSpec, random);
|
||||
|
||||
KeyPair keyPair = g.generateKeyPair();
|
||||
|
||||
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
|
||||
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
|
||||
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
|
||||
keyAgreement.init(keyPair.getPrivate());
|
||||
|
||||
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
|
||||
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec);
|
||||
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
|
||||
|
||||
keyAgreement.doPhase(cardKey, true);
|
||||
secret = keyAgreement.generateSecret();
|
||||
@@ -96,11 +96,19 @@ public class SecureChannelSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pairing index
|
||||
* @return the pairing index
|
||||
* Returns the pairing information
|
||||
* @return the pairing information
|
||||
*/
|
||||
public byte getPairingIndex() {
|
||||
return pairingIndex;
|
||||
public Pairing getPairing() {
|
||||
return pairing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets pairing information needed to open a secure channel.
|
||||
* @param pairing the pairing information
|
||||
*/
|
||||
public void setPairing(Pairing pairing) {
|
||||
this.pairing = pairing;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,11 +116,10 @@ public class SecureChannelSession {
|
||||
* 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);
|
||||
APDUResponse response = openSecureChannel(apduChannel, pairing.getPairingIndex(), publicKey);
|
||||
|
||||
if (response.getSw() != 0x9000) {
|
||||
throw new IOException("OPEN SECURE CHANNEL failed");
|
||||
@@ -140,14 +147,14 @@ public class SecureChannelSession {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA512");
|
||||
md.update(secret);
|
||||
md.update(pairingKey);
|
||||
md.update(pairing.getPairingKey());
|
||||
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");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
|
||||
open = true;
|
||||
} catch(Exception e) {
|
||||
@@ -188,7 +195,7 @@ public class SecureChannelSession {
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA256");
|
||||
md = MessageDigest.getInstance("SHA256", "BC");
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
@@ -211,8 +218,7 @@ public class SecureChannelSession {
|
||||
|
||||
respData = resp.getData();
|
||||
md.update(sharedSecret);
|
||||
pairingKey = md.digest(Arrays.copyOfRange(respData, 1, respData.length));
|
||||
pairingIndex = respData[0];
|
||||
pairing = new Pairing(md.digest(Arrays.copyOfRange(respData, 1, respData.length)), respData[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +228,7 @@ public class SecureChannelSession {
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void autoUnpair(CardChannel apduChannel) throws IOException {
|
||||
APDUResponse resp = unpair(apduChannel, pairingIndex);
|
||||
APDUResponse resp = unpair(apduChannel, pairing.getPairingIndex());
|
||||
|
||||
if (resp.getSw() != 0x9000) {
|
||||
throw new IOException("Unpairing failed");
|
||||
@@ -281,8 +287,8 @@ public class SecureChannelSession {
|
||||
* @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);
|
||||
APDUCommand pair = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
|
||||
return transmit(apduChannel, pair);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,22 +300,21 @@ public class SecureChannelSession {
|
||||
* @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);
|
||||
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
|
||||
return transmit(apduChannel, unpair);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
if (i != pairing.getPairingIndex()) {
|
||||
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
|
||||
transmit(apduChannel, unpair).checkOK();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +439,7 @@ public class SecureChannelSession {
|
||||
random.nextBytes(iv);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionEncKey = new SecretKeySpec(secret, "AES");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
initData = sessionCipher.doFinal(initData);
|
||||
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
|
||||
@@ -453,7 +458,7 @@ public class SecureChannelSession {
|
||||
* would only make things wrong.
|
||||
*
|
||||
*/
|
||||
void setOpen() {
|
||||
protected void setOpen() {
|
||||
open = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Tiny BER-TLV implementation. Not for general usage, but fast and easy to use for this project.
|
||||
*/
|
||||
public class TinyBERTLV {
|
||||
public static final byte TLV_BOOL = (byte) 0x01;
|
||||
public static final byte TLV_INT = (byte) 0x02;
|
||||
|
||||
public static final int END_OF_TLV = (int) 0xffffffff;
|
||||
|
||||
private byte[] buffer;
|
||||
private int pos;
|
||||
|
||||
public TinyBERTLV(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters a constructed TLV with the given tag
|
||||
*
|
||||
* @param tag the tag to enter
|
||||
* @return the length of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
*/
|
||||
public int enterConstructed(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
return readLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a primitive TLV with the given tag
|
||||
*
|
||||
* @param tag the tag to read
|
||||
* @return the body of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
*/
|
||||
public byte[] readPrimitive(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
int len = readLength();
|
||||
pos += len;
|
||||
return Arrays.copyOfRange(buffer, (pos - len), pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a boolean TLV.
|
||||
*
|
||||
* @return the boolean value of the TLV
|
||||
* @throws IllegalArgumentException if the next tag is not a boolean
|
||||
*/
|
||||
public boolean readBoolean() throws IllegalArgumentException {
|
||||
byte[] val = readPrimitive(TLV_BOOL);
|
||||
return ((val[0] & 0xff) == 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an integer TLV.
|
||||
*
|
||||
* @return the integer value of the TLV
|
||||
* @throws IllegalArgumentException if the next tlv is not an integer or is of unsupported length
|
||||
*/
|
||||
public int readInt() throws IllegalArgumentException {
|
||||
byte[] val = readPrimitive(TLV_INT);
|
||||
|
||||
switch (val.length) {
|
||||
case 1:
|
||||
return val[0] & 0xff;
|
||||
case 2:
|
||||
return ((val[0] & 0xff) << 8) | (val[1] & 0xff);
|
||||
case 3:
|
||||
return ((val[0] & 0xff) << 16) | ((val[1] & 0xff) << 8) | (val[2] & 0xff);
|
||||
case 4:
|
||||
return ((val[0] & 0xff) << 24) | ((val[1] & 0xff) << 16) | ((val[2] & 0xff) << 8) | (val[3] & 0xff);
|
||||
default:
|
||||
throw new IllegalArgumentException("Integers of length " + val.length + " are unsupported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level method to unread the last read tag. Only valid if the previous call was readTag(). Does nothing if the
|
||||
* end of the TLV has been reached.
|
||||
*/
|
||||
public void unreadLastTag() {
|
||||
if (pos < buffer.length) {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next tag. The current implementation only reads tags on one byte. Can be extended if needed.
|
||||
*
|
||||
* @return the tag
|
||||
*/
|
||||
public int readTag() {
|
||||
return (pos < buffer.length) ? buffer[pos++] : END_OF_TLV;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed.
|
||||
*
|
||||
* @return the tag
|
||||
*/
|
||||
public int readLength() {
|
||||
int len = buffer[pos++] & 0xff;
|
||||
|
||||
if (len == 0x81) {
|
||||
len = buffer[pos++] & 0xff;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
private void checkTag(int expected, int actual) throws IllegalArgumentException {
|
||||
if (expected != actual) {
|
||||
unreadLastTag();
|
||||
throw new IllegalArgumentException("Expected tag: " + expected + ", received: " + actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.security.*;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Crypto utilities for Global Platform.
|
||||
*/
|
||||
public class Crypto {
|
||||
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
public static long PIN_BOUND = 999999L;
|
||||
public static long PUK_BOUND = 999999999999L;
|
||||
|
||||
private static boolean bouncyCastleLoaded = false;
|
||||
|
||||
public static void addBouncyCastleProvider() {
|
||||
if (!bouncyCastleLoaded) {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
bouncyCastleLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a session key for SCP02.
|
||||
*
|
||||
* @param cardKey the key to derive
|
||||
* @param seq the sequence number
|
||||
* @param purposeData purpose data
|
||||
*
|
||||
* @return the derived key
|
||||
*/
|
||||
public static byte[] deriveSCP02SessionKey(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", "BC");
|
||||
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);
|
||||
} catch (NoSuchProviderException e) {
|
||||
throw new RuntimeException("SpongyCastle not installed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Padding for SCP02 encryption.
|
||||
*
|
||||
* @param data data to pad
|
||||
* @return the padded data
|
||||
*/
|
||||
public static byte[] appendDESPadding(byte[] data) {
|
||||
int paddingLength = 8 - (data.length % 8);
|
||||
byte[] newData = new byte[data.length + paddingLength];
|
||||
System.arraycopy(data, 0, newData, 0, data.length);
|
||||
newData[data.length] = (byte)0x80;
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a card cryptogram received using during SCP02 channel establishment.
|
||||
*
|
||||
* @param key the key
|
||||
* @param hostChallenge host challenge
|
||||
* @param cardChallenge card challenge
|
||||
* @param cardCryptogram cryptogram to verify
|
||||
* @return true if correct, false otherwise
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a 3DES MAC for SCP02 channel establishment
|
||||
*
|
||||
* @param keyData key
|
||||
* @param data data to sign
|
||||
* @param iv IV
|
||||
* @return the MAC
|
||||
*/
|
||||
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", "BC");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 3DES MAC for SCP02 communication
|
||||
*
|
||||
* @param keyData key
|
||||
* @param data data to sign
|
||||
* @param iv IV
|
||||
* @return the MAC
|
||||
*/
|
||||
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", "BC");
|
||||
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
|
||||
|
||||
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
|
||||
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used during key derivation .
|
||||
*
|
||||
* @param keyData the key data
|
||||
*
|
||||
* @return the resized key
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used during MAC generation.
|
||||
*
|
||||
* @param keyData the key data
|
||||
*
|
||||
* @return the resized key
|
||||
*/
|
||||
private static byte[] resizeKey8(byte[] keyData) {
|
||||
byte[] key = new byte[8];
|
||||
System.arraycopy(keyData, 0, key, 0, 8);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the ICV
|
||||
*
|
||||
* @param macKeyData MAC Key
|
||||
* @param mac mac
|
||||
*
|
||||
* @return encrypted ICV
|
||||
*/
|
||||
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding", "BC");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the given number of random bytes.
|
||||
*
|
||||
* @param length the number of bytes to generate
|
||||
* @return random bytes
|
||||
*/
|
||||
public static byte[] randomBytes(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte data[] = new byte[length];
|
||||
random.nextBytes(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random long between 0 and then given boundary
|
||||
*
|
||||
* @param bound the maximum value to generate
|
||||
* @return the random number
|
||||
*/
|
||||
public static long randomLong(long bound) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
return Math.abs(random.nextLong()) % bound;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
import im.status.keycard.applet.Identifiers;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
|
||||
/**
|
||||
* Command set used for loading, installing and removing applets and packages. This class is generic and can work with
|
||||
* any package and applet, but utility methods specific to the Keycard have been provided.
|
||||
*/
|
||||
public class GlobalPlatformCommandSet {
|
||||
static final byte INS_SELECT = (byte) 0xA4;
|
||||
static final byte INS_INITIALIZE_UPDATE = (byte) 0x50;
|
||||
static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82;
|
||||
static final byte INS_DELETE = (byte) 0xE4;
|
||||
static final byte INS_INSTALL = (byte) 0xE6;
|
||||
static final byte INS_LOAD = (byte) 0xE8;
|
||||
|
||||
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
|
||||
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
|
||||
static final byte INSTALL_FOR_LOAD_P1 = (byte) 0x02;
|
||||
static final byte INSTALL_FOR_INSTALL_P1 = (byte) 0x0C;
|
||||
static final byte LOAD_P1_MORE_BLOCKS = (byte) 0x00;
|
||||
static final byte LOAD_P1_LAST_BLOCK = (byte) 0x80;
|
||||
|
||||
private final CardChannel apduChannel;
|
||||
private SecureChannel secureChannel;
|
||||
private SCP02Keys cardKeys;
|
||||
private Session session;
|
||||
|
||||
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
|
||||
|
||||
/**
|
||||
* Constructs a new command set with the given CardChannel.
|
||||
*
|
||||
* @param apduChannel the channel to the card
|
||||
*/
|
||||
public GlobalPlatformCommandSet(CardChannel apduChannel) {
|
||||
this.apduChannel = apduChannel;
|
||||
this.cardKeys = new SCP02Keys(testKey, testKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the ISD of the card.
|
||||
*
|
||||
* @return the card response
|
||||
*
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse select() throws IOException {
|
||||
APDUCommand cmd = new APDUCommand(0x00, INS_SELECT, SELECT_P1_BY_NAME, 0, new byte[0]);
|
||||
return apduChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an INITIALIZE UPDATE command. Use the openSecureChannel method instead of calling this directly, unless you
|
||||
* need to use a specific host challenge.
|
||||
*
|
||||
* @param hostChallenge the host challenge.
|
||||
* @return the card response
|
||||
*
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse initializeUpdate(byte[] hostChallenge) throws IOException, APDUException {
|
||||
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
|
||||
APDUResponse resp = apduChannel.send(cmd);
|
||||
if (resp.isOK()) {
|
||||
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
|
||||
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXTERNAL AUTHENTICATE command. Use the openSecureChannel method instead of calling this directly, unless you
|
||||
* need to use a specific host challenge.
|
||||
*
|
||||
* @param hostChallenge the host challenge.
|
||||
* @return the card response
|
||||
*
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse externalAuthenticate(byte[] hostChallenge) throws IOException {
|
||||
byte[] cardChallenge = this.session.getCardChallenge();
|
||||
byte[] data = new byte[cardChallenge.length + hostChallenge.length];
|
||||
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
|
||||
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
|
||||
|
||||
byte[] paddedData = Crypto.appendDESPadding(data);
|
||||
byte[] hostCryptogram = Crypto.mac3des(this.session.getKeys().encKeyData, paddedData, Crypto.NullBytes8);
|
||||
|
||||
APDUCommand cmd = new APDUCommand(0x84, INS_EXTERNAL_AUTHENTICATE, EXTERNAL_AUTHENTICATE_P1, 0, hostCryptogram);
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SCP02 secure channel with default keys.
|
||||
*
|
||||
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void openSecureChannel() throws APDUException, IOException {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] hostChallenge = new byte[8];
|
||||
random.nextBytes(hostChallenge);
|
||||
initializeUpdate(hostChallenge).checkOK();
|
||||
externalAuthenticate(hostChallenge).checkOK();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Keycard applet instance.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deleteKeycardInstance() throws IOException {
|
||||
return delete(Identifiers.getKeycardInstanceAID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the NDEF applet instance.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deleteNDEFInstance() throws IOException {
|
||||
return delete(Identifiers.NDEF_INSTANCE_AID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Keycard package.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deleteKeycardPackage() throws IOException {
|
||||
return delete(Identifiers.PACKAGE_AID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Keycard package and all applets installed from it. This is the method to use to remove a Keycard
|
||||
* installation.
|
||||
*
|
||||
* @throws APDUException one of the DELETE commands failed
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void deleteKeycardInstancesAndPackage() throws IOException, APDUException {
|
||||
deleteNDEFInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteKeycardInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DELETE APDU with the given AID
|
||||
* @param aid the AID to the delete
|
||||
* @return the raw card response
|
||||
*
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse delete(byte[] aid) throws IOException {
|
||||
byte[] data = new byte[aid.length + 2];
|
||||
data[0] = 0x4F;
|
||||
data[1] = (byte) aid.length;
|
||||
System.arraycopy(aid, 0, data, 2, aid.length);
|
||||
|
||||
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, 0, data);
|
||||
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Keycard package.
|
||||
*
|
||||
* @param in the CAP file as an InputStream
|
||||
* @param cb the progress callback
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException one of the INSTALL [for Load] or LOAD commands failed
|
||||
*/
|
||||
public void loadKeycardPackage(InputStream in, LoadCallback cb) throws IOException, APDUException {
|
||||
installForLoad(Identifiers.PACKAGE_AID).checkOK();
|
||||
|
||||
Load load = new Load(in);
|
||||
|
||||
byte[] block;
|
||||
int steps = load.blocksCount();
|
||||
|
||||
while((block = load.nextDataBlock()) != null) {
|
||||
load(block, (load.getCount() - 1), load.hasMore()).checkOK();
|
||||
cb.blockLoaded(load.getCount(), steps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an INSTALL [for LOAD] APDU. Use only if loading something other than the Keycard package.
|
||||
*
|
||||
* @param aid the AID
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse installForLoad(byte[] aid) throws IOException {
|
||||
return installForLoad(aid, new byte[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an INSTALL [for LOAD] APDU with package extradition. Use only if loading something other than the Keycard package.
|
||||
*
|
||||
* @param aid the AID
|
||||
* @param sdaid the AID of the SD target of the extradition
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse installForLoad(byte[] aid, byte[] sdaid) throws IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
data.write(aid.length);
|
||||
data.write(aid);
|
||||
data.write(sdaid.length);
|
||||
data.write(sdaid);
|
||||
|
||||
// empty hash length and hash
|
||||
data.write(0x00);
|
||||
data.write(0x00);
|
||||
data.write(0x00);
|
||||
|
||||
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_LOAD_P1, 0, data.toByteArray());
|
||||
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a single LOAD APDU. Use only if loading something other than the Keycard package.
|
||||
*
|
||||
* @param data the data of the block
|
||||
* @param count the block number
|
||||
* @param hasMoreBlocks whether there are more blocks coming or not
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse load(byte[] data, int count, boolean hasMoreBlocks) throws IOException {
|
||||
int p1 = hasMoreBlocks ? LOAD_P1_MORE_BLOCKS : LOAD_P1_LAST_BLOCK;
|
||||
APDUCommand cmd = new APDUCommand(0x80, INS_LOAD, p1, count, data);
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an INSTALL [for Install & Make Selectable] command. Use only if not installing applets part of the Keycard
|
||||
* package
|
||||
*
|
||||
* @param packageAID the package AID
|
||||
* @param appletAID the applet AID
|
||||
* @param instanceAID the instance AID
|
||||
* @param params the installation parameters
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse installForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) throws IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
data.write(packageAID.length);
|
||||
data.write(packageAID);
|
||||
data.write(appletAID.length);
|
||||
data.write(appletAID);
|
||||
data.write(instanceAID.length);
|
||||
data.write(instanceAID);
|
||||
|
||||
byte[] privileges = new byte[]{0x00};
|
||||
data.write(privileges.length);
|
||||
data.write(privileges);
|
||||
|
||||
byte[] fullParams = new byte[2 + params.length];
|
||||
fullParams[0] = (byte) 0xC9;
|
||||
fullParams[1] = (byte) params.length;
|
||||
System.arraycopy(params, 0, fullParams, 2, params.length);
|
||||
|
||||
data.write(fullParams.length);
|
||||
data.write(fullParams);
|
||||
|
||||
// empty perform token
|
||||
data.write(0x00);
|
||||
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_INSTALL_P1, 0, data.toByteArray());
|
||||
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the NDEF applet from the Keycard package.
|
||||
*
|
||||
* @param ndefRecord the initial NDEF record. Can be a zero-length array but not null
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse installNDEFApplet(byte[] ndefRecord) throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.NDEF_AID, Identifiers.NDEF_INSTANCE_AID, ndefRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Keycard applet.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse installKeycardApplet() throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.KEYCARD_AID, Identifiers.getKeycardInstanceAID(), new byte[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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;
|
||||
|
||||
/**
|
||||
* A loadable CAP file.
|
||||
*/
|
||||
public class Load {
|
||||
static final byte CLA = (byte) 0x80;
|
||||
static final byte INS = (byte) 0xE8;
|
||||
|
||||
static final int BLOCK_SIZE = 247; // 255 - 8 bytes for MAC
|
||||
|
||||
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
|
||||
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
|
||||
|
||||
private int offset;
|
||||
private int count;
|
||||
private byte[] fullData;
|
||||
|
||||
/**
|
||||
* Reads a CAP file from the given input stream.
|
||||
*
|
||||
* @param in the inpu stream
|
||||
* @throws FileNotFoundException
|
||||
* @throws IOException
|
||||
*/
|
||||
public Load(InputStream in) throws FileNotFoundException, IOException {
|
||||
this.offset = 0;
|
||||
this.count = 0;
|
||||
Map<String, byte[]> files = this.loadFiles(in);
|
||||
in.close();
|
||||
this.fullData = this.getCode(files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the components of the CAP file
|
||||
* @param in the input stream
|
||||
* @return the map of component name and values
|
||||
*
|
||||
* @throws IOException IO error
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The basename of the zip entry
|
||||
* @param path the path
|
||||
* @return the base name
|
||||
*/
|
||||
private String baseName(String path) {
|
||||
String[] parts = path.split("[/.]");
|
||||
return parts[parts.length - 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of blocks needed to load the entire file. Keeps in account the overhead of SCP02 secure channel
|
||||
*
|
||||
* @return the block count
|
||||
*/
|
||||
public int blocksCount() {
|
||||
return (int) Math.ceil(this.fullData.length / (float) BLOCK_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next data block
|
||||
*
|
||||
* @return the data block
|
||||
*/
|
||||
public byte[] nextDataBlock() {
|
||||
if (this.offset >= this.fullData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int rangeEnd = this.offset + BLOCK_SIZE;
|
||||
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);
|
||||
|
||||
|
||||
this.count++;
|
||||
this.offset += size;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if more blocks are present, false otherwise.
|
||||
*
|
||||
* @return true if more blocks are present, false otherwise.
|
||||
*/
|
||||
public boolean hasMore() {
|
||||
return this.offset < this.fullData.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the length of the load TLV component
|
||||
*
|
||||
* @param length the length as integer
|
||||
* @return the length encoded as for BER-TLV
|
||||
*/
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the CAP section in a single block.
|
||||
*
|
||||
* @param files the components to serialize
|
||||
* @return the serialized load file
|
||||
*
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current block number
|
||||
*
|
||||
* @return the current block number
|
||||
*/
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
/**
|
||||
* Callback interface using during package loading process.
|
||||
*/
|
||||
public interface LoadCallback {
|
||||
/**
|
||||
* Called when a block is loaded.
|
||||
*
|
||||
* @param loadedBlock The number of the loaded block (1 based)
|
||||
* @param blockCount the total number of blocks.
|
||||
*/
|
||||
void blockLoaded(int loadedBlock, int blockCount);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
/**
|
||||
* Keeps keys for SCP02.
|
||||
*/
|
||||
public class SCP02Keys {
|
||||
public byte[] encKeyData;
|
||||
public byte[] macKeyData;
|
||||
|
||||
/**
|
||||
* Constructor. Takes the ENC and MAC keys.
|
||||
*
|
||||
* @param encKeyData encryption key
|
||||
* @param macKeyData mac key
|
||||
*/
|
||||
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
|
||||
this.encKeyData = encKeyData;
|
||||
this.macKeyData = macKeyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The encryption key
|
||||
* @return the encryption key
|
||||
*/
|
||||
public byte[] getEncKeyData() {
|
||||
return encKeyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The MAC key
|
||||
*
|
||||
* @return the MAC key
|
||||
*/
|
||||
public byte[] getMacKeyData() {
|
||||
return macKeyData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
|
||||
/**
|
||||
* Adds a SCP02 MAC to APDUs.
|
||||
*/
|
||||
public class SCP02Wrapper {
|
||||
private byte[] macKeyData;
|
||||
private byte[] icv;
|
||||
|
||||
/**
|
||||
* Constructs a new SCP02Wrapper.
|
||||
*
|
||||
* @param macKeyData the MAC key
|
||||
*/
|
||||
public SCP02Wrapper(byte[] macKeyData) {
|
||||
this.macKeyData = macKeyData;
|
||||
this.icv = Crypto.NullBytes8.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an APDU with SCP02 MAC
|
||||
* @param cmd the APDU to wrap
|
||||
* @return the wrapped APDU
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ICV
|
||||
* @return the ICV
|
||||
*/
|
||||
public byte[] getICV() {
|
||||
return this.icv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUException;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
|
||||
/**
|
||||
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the scure channel.
|
||||
*/
|
||||
public class SecureChannel {
|
||||
private CardChannel channel;
|
||||
private SCP02Wrapper wrapper;
|
||||
|
||||
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};
|
||||
|
||||
/**
|
||||
* Constructs an SCP02 secure channel, wrapping a regular CardChannel.
|
||||
*
|
||||
* @param channel the channel to wrap
|
||||
* @param keys the keys
|
||||
*/
|
||||
public SecureChannel(CardChannel channel, SCP02Keys keys) {
|
||||
this.channel = channel;
|
||||
this.wrapper = new SCP02Wrapper(keys.getMacKeyData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Protects the given command with SCP02 and forwards it to the underlying CardChannel.
|
||||
*
|
||||
* @param cmd the command to send
|
||||
* @return the response from the card
|
||||
*
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
|
||||
return this.channel.send(wrappedCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the card challenge and builds an SCP02 session object.
|
||||
*
|
||||
* @param hostChallenge the host challenge
|
||||
* @param cardKeys the SCP02 keys
|
||||
* @param resp the response from the card to the INITIALIZE UPDATE oommand
|
||||
* @return the Session object built on succesful verification
|
||||
* @throws APDUException communication error
|
||||
*/
|
||||
public static Session verifyChallenge(byte[] hostChallenge, SCP02Keys cardKeys, APDUResponse resp) throws APDUException {
|
||||
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
|
||||
throw new APDUException(resp.getSw(), "security condition 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.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
|
||||
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
|
||||
|
||||
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
|
||||
|
||||
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
|
||||
if (!verified) {
|
||||
throw new APDUException("error verifying card cryptogram.");
|
||||
}
|
||||
|
||||
return new Session(sessionKeys, cardChallenge);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package im.status.keycard.globalplatform;
|
||||
|
||||
/**
|
||||
* SCP02 Session.
|
||||
*/
|
||||
public class Session {
|
||||
private SCP02Keys keys;
|
||||
private byte[] cardChallenge;
|
||||
|
||||
/**
|
||||
* Constructs the SCP02 session.
|
||||
*
|
||||
* @param keys the session keys
|
||||
* @param cardChallenge the card challenge
|
||||
*/
|
||||
public Session(SCP02Keys keys, byte[] cardChallenge) {
|
||||
this.keys = keys;
|
||||
this.cardChallenge = cardChallenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* The SCP02 keys
|
||||
* @return SCP02 keys
|
||||
*/
|
||||
public SCP02Keys getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* The card challenge
|
||||
* @return card challenge
|
||||
*/
|
||||
public byte[] getCardChallenge() {
|
||||
return cardChallenge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* ISO7816-4 APDU.
|
||||
*/
|
||||
public class APDUCommand {
|
||||
protected int cla;
|
||||
protected int ins;
|
||||
protected int p1;
|
||||
protected int p2;
|
||||
protected int lc;
|
||||
protected byte[] data;
|
||||
protected boolean needsLE;
|
||||
|
||||
/**
|
||||
* Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
|
||||
*
|
||||
* @param cla class byte
|
||||
* @param ins instruction code
|
||||
* @param p1 P1 parameter
|
||||
* @param p2 P2 parameter
|
||||
* @param data the APDU data
|
||||
*/
|
||||
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||
this(cla, ins, p1, p2, data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
|
||||
* The LE byte, if sent, is set to 0.
|
||||
*
|
||||
* @param cla class byte
|
||||
* @param ins instruction code
|
||||
* @param p1 P1 parameter
|
||||
* @param p2 P2 parameter
|
||||
* @param data the APDU data
|
||||
* @param needsLE whether the LE byte should be sent or not
|
||||
*/
|
||||
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
|
||||
this.cla = cla & 0xff;
|
||||
this.ins = ins & 0xff;
|
||||
this.p1 = p1 & 0xff;
|
||||
this.p2 = p2 & 0xff;
|
||||
this.data = data;
|
||||
this.needsLE = needsLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the APDU in order to send it to the card.
|
||||
*
|
||||
* @return the byte array representation of the APDU
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CLA of the APDU
|
||||
*
|
||||
* @return the CLA of the APDU
|
||||
*/
|
||||
public int getCla() {
|
||||
return cla;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the INS of the APDU
|
||||
*
|
||||
* @return the INS of the APDU
|
||||
*/
|
||||
public int getIns() {
|
||||
return ins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the P1 of the APDU
|
||||
*
|
||||
* @return the P1 of the APDU
|
||||
*/
|
||||
public int getP1() {
|
||||
return p1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the P2 of the APDU
|
||||
*
|
||||
* @return the P2 of the APDU
|
||||
*/
|
||||
public int getP2() {
|
||||
return p2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data field of the APDU
|
||||
*
|
||||
* @return the data field of the APDU
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether LE is sent or not.
|
||||
*
|
||||
* @return whether LE is sent or not
|
||||
*/
|
||||
public boolean getNeedsLE() {
|
||||
return this.needsLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
/**
|
||||
* Exception thrown when the response APDU from the card contains unexpected SW or data.
|
||||
*/
|
||||
public class APDUException extends Exception {
|
||||
public final int sw;
|
||||
|
||||
/**
|
||||
* Creates an exception with SW and message.
|
||||
*
|
||||
* @param sw the status word
|
||||
* @param message a descriptive message of the error
|
||||
*/
|
||||
public APDUException(int sw, String message) {
|
||||
super(message + ", 0x" + String.format("%04X", sw));
|
||||
this.sw = sw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an exception with a message.
|
||||
*
|
||||
* @param message a descriptive message of the error
|
||||
*/
|
||||
public APDUException(String message) {
|
||||
super(message);
|
||||
this.sw = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
/**
|
||||
* ISO7816-4 APDU response.
|
||||
*/
|
||||
public class APDUResponse {
|
||||
public static final int SW_OK = 0x9000;
|
||||
public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
|
||||
public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
|
||||
public static final int SW_CARD_LOCKED = 0x6283;
|
||||
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
|
||||
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
|
||||
public static final int SW_WRONG_PIN_MASK = 0x63C0;
|
||||
|
||||
private byte[] apdu;
|
||||
private byte[] data;
|
||||
private int sw;
|
||||
private int sw1;
|
||||
private int sw2;
|
||||
|
||||
/**
|
||||
* Creates an APDU object by parsing the raw response from the card.
|
||||
*
|
||||
* @param apdu the raw response from the card.
|
||||
*/
|
||||
public APDUResponse(byte[] apdu) {
|
||||
if (apdu.length < 2) {
|
||||
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
|
||||
}
|
||||
this.apdu = apdu;
|
||||
this.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the APDU response, separating the response data from SW.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the SW is 0x9000.
|
||||
*
|
||||
* @return true if the SW is 0x9000.
|
||||
*/
|
||||
public boolean isOK() {
|
||||
return this.sw == SW_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is 0x9000. Throws an exception if it isn't
|
||||
*
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkOK() throws APDUException {
|
||||
return this.checkSW(SW_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is contained in the given list. Throws an exception if it isn't.
|
||||
*
|
||||
* @param codes the list of SWs to match.
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkSW(int... codes) throws APDUException {
|
||||
for (int code : codes) {
|
||||
if (this.sw == code) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.sw) {
|
||||
case SW_SECURITY_CONDITION_NOT_SATISFIED:
|
||||
throw new APDUException(this.sw, "security condition not satisfied");
|
||||
case SW_AUTHENTICATION_METHOD_BLOCKED:
|
||||
throw new APDUException(this.sw, "authentication method blocked");
|
||||
default:
|
||||
throw new APDUException(this.sw, "Unexpected error SW");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
|
||||
*
|
||||
* @throws WrongPINException wrong PIN
|
||||
* @throws APDUException unexpected response
|
||||
*/
|
||||
public APDUResponse checkAuthOK() throws WrongPINException, APDUException {
|
||||
if ((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
|
||||
throw new WrongPINException(sw2 & 0x0F);
|
||||
} else {
|
||||
return checkOK();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data field of this APDU.
|
||||
*
|
||||
* @return the data field of this APDU
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Status Word.
|
||||
*
|
||||
* @return the status word
|
||||
*/
|
||||
public int getSw() {
|
||||
return this.sw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SW1 byte
|
||||
* @return SW1
|
||||
*/
|
||||
public int getSw1() {
|
||||
return this.sw1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SW2 byte
|
||||
* @return SW2
|
||||
*/
|
||||
public int getSw2() {
|
||||
return this.sw2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw unparsed response.
|
||||
*
|
||||
* @return raw APDU data
|
||||
*/
|
||||
public byte[] getBytes() {
|
||||
return this.apdu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A channel to transcieve ISO7816-4 APDUs.
|
||||
*/
|
||||
public interface CardChannel {
|
||||
/**
|
||||
* Sends the given C-APDU and returns an R-APDU.
|
||||
*
|
||||
* @param cmd the command to send
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
APDUResponse send(APDUCommand cmd) throws IOException;
|
||||
|
||||
/**
|
||||
* True if connected, false otherwise
|
||||
* @return true if connected, false otherwise
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Returns the iteration count for deriving the pairing key from the pairing password. The default is 50000 and is
|
||||
* should only be changed for devices where the PBKDF2 is calculated on-board and the resource do not permit a
|
||||
* high iteration count. If a lower count is used other security mechanism should be used to prevent brute force
|
||||
* attacks.
|
||||
*
|
||||
* @return the iteration count
|
||||
*/
|
||||
default int pairingPasswordPBKDF2IterationCount() {
|
||||
return 50000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
/**
|
||||
* Listener for card connection events.
|
||||
*/
|
||||
public interface CardListener {
|
||||
/**
|
||||
* Executes when the card channel is connected.
|
||||
*
|
||||
* @param channel the connected card channel
|
||||
*/
|
||||
void onConnected(CardChannel channel);
|
||||
|
||||
/**
|
||||
* Executes when a previously connected card is disconnected.
|
||||
*/
|
||||
void onDisconnected();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
/**
|
||||
* Exception thrown when checking PIN/PUK
|
||||
*/
|
||||
public class WrongPINException extends APDUException {
|
||||
private int retryAttempts;
|
||||
|
||||
/**
|
||||
* Construct an exception with the given number of retry attempts.
|
||||
*
|
||||
* @param retryAttempts the number of retry attempts
|
||||
*/
|
||||
public WrongPINException(int retryAttempts) {
|
||||
super("Wrong PIN");
|
||||
this.retryAttempts = retryAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of available retry attempts.
|
||||
*
|
||||
* @return the number of retry attempts
|
||||
*/
|
||||
public int getRetryAttempts() {
|
||||
return retryAttempts;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
include ':lib'
|
||||
include ':demo'
|
||||
include 'lib'
|
||||
include 'android'
|
||||
include 'desktop'
|
||||
include 'demo-android'
|
||||
|
||||