Compare commits

..
34 Commits
Author SHA1 Message Date
Michele Balistreri 0d16541875 2.0.0 release 2018-12-14 13:49:59 +03:00
Michele Balistreri 171220bd79 change AIDs 2018-12-14 13:21:36 +03:00
Bitgamma b08b1dc7ce Unified sdk (#12)
* unifying Android and Desktop SDK

* implement desktop SDK adapter

* updating declarations

* change include syntax

* following jitpack.io guide for Android libraries

* add install task to all artefacts, add javadoc generation

* fixing javadoc

* use explicit provider "SC" instead of relying on order

* move to BouncyCastle for desktop compatibility

* improve documentation
2018-12-11 11:50:53 +03:00
Bitgamma 07fc087cb3 [WIP] rename hardwallet to keycard (#11)
* rename hardwallet to keycard

* rename hardwallet to keycard

* rename hardwallet to keycard

* change constants

* add convenience GlobalPlatform methods

* add javadoc to all classes
2018-12-07 16:44:02 +03:00
Michele Balistreri c749e5a744 update EXPORT KEY command 2018-11-30 11:11:38 +03:00
Michele Balistreri 02fee8edd2 update EXPORT KEY command 2018-11-30 10:43:32 +03:00
Michele Balistreri c31df742dd filter idea folder 2018-11-27 14:35:44 +03:00
Bitgamma 3c759152e3 Delete codeStyleConfig.xml 2018-11-27 14:33:39 +03:00
Andrea Franz 735ccd80d9 Merge pull request #10 from status-im/feature/globalplatform-secure-channel
add installation commands
2018-11-27 12:21:45 +01:00
Andrea Franz 439237a75a add blocksCount method to Load class 2018-11-27 12:19:21 +01:00
Andrea Franz 5a2204bbcb add checkSW method 2018-11-27 12:02:52 +01:00
Andrea Franz 918aec72ac add installation commands 2018-11-27 12:02:52 +01:00
Michele Balistreri 145fe4b554 add to demo 2018-11-26 20:05:58 +03:00
Michele Balistreri 1575dc9d86 add BIP32KeyPair 2018-11-26 19:32:27 +03:00
Michele Balistreri ae65a94705 add missing import method 2018-11-26 11:45:54 +03:00
Andrea Franz a9726f3c50 Merge pull request #9 from status-im/feature/globalplatform-secure-channel
implement globalplatform secure channel commands
2018-11-22 17:20:54 +01:00
Michele Balistreri a85e1de2e6 add duplicate key 2018-11-22 17:59:20 +03:00
Michele Balistreri 3cad21254c add static method in Mnemonic to accept external phrase 2018-11-22 15:48:06 +03:00
Andrea Franz e781d5551f remove duplicated generatePairingKey 2018-11-22 13:09:11 +01:00
Andrea Franz 1ae29cd1cd rename Keys to SCP02Keys 2018-11-22 13:07:43 +01:00
Andrea Franz 346ab46187 rename APDUWrapper to SCP02Wrapper 2018-11-22 13:01:09 +01:00
Andrea Franz 41f2473f64 rename smartcard pkg to globalplatform 2018-11-22 12:57:19 +01:00
Andrea Franz 4399be35a4 rename deriveKey to deriveSCP02SessionKey 2018-11-22 12:55:58 +01:00
Andrea Franz ed7630bada improve appendDESPadding from the imported library 2018-11-22 12:48:03 +01:00
Michele Balistreri 03b0a56a09 fixed 2018-11-22 13:32:30 +03:00
Michele Balistreri a66f921ee8 add Mnemonic class 2018-11-22 11:39:49 +03:00
Andrea Franz 801accdde3 implement globalplatform secure channel commands 2018-11-21 18:14:01 +01:00
Michele Balistreri 002ade1c74 add conversion from keypath data to string 2018-11-16 11:12:33 +03:00
Andrea Franz f646e5d8ee Card listener (#8)
* update listener interface to have onDisconnected callback

* allow custom loop time

* check that cardListener is not null before invoking it
2018-11-15 21:25:08 +03:00
Michele Balistreri 931b6608d5 Closes #6, #7 2018-11-15 20:16:18 +03:00
Michele Balistreri 70b50c8989 Closes #5 2018-11-14 18:27:39 +03:00
Michele Balistreri 8b4e66e50b Add higher-level classes and improve the demo 2018-11-14 17:52:59 +03:00
Michele Balistreri f4fd086467 Closes #3 2018-11-14 11:41:00 +03:00
Bitgamma eb9f3f1350 [WIP] Update to version 2 (#2)
* v2

* add SET NDEF

* make a few constants public

* improve secure channel handling

* improve secure channel handling

* improve secure channel handling
2018-11-13 16:33:44 +03:00
69 changed files with 3295 additions and 601 deletions
+1 -3
View File
@@ -1,9 +1,7 @@
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.idea
.DS_Store
/build
/captures
+28
View File
@@ -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.0.0'
}
```
### on the desktop
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:2.0.0'
}
```
+8
View File
@@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
+50
View File
@@ -0,0 +1,50 @@
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 2
versionName "2.0"
}
}
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.addSpongyCastleProvider();
}
/**
* 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;
}
}
+1 -4
View File
@@ -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'
}
}
@@ -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 2
versionName "2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -21,8 +21,10 @@ android {
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation project(':lib')
implementation project(':android')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
@@ -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,167 @@
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.spongycastle.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");
}
// 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());
// 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()));
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkOK();
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()) {
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()));
// 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);
}
}
}
-3
View File
@@ -1,3 +0,0 @@
<resources>
<string name="app_name">HardwalletLiteAndroid</string>
</resources>
+8
View File
@@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
+21
View File
@@ -0,0 +1,21 @@
apply plugin: 'java'
apply plugin: 'maven'
dependencies {
compile project(':lib')
}
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,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.addSpongyCastleProvider();
}
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;
}
}
+18 -23
View File
@@ -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,118 @@
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;
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;
/**
* 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;
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);
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;
}
}
@@ -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,96 @@
package im.status.keycard.applet;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import java.io.IOException;
import java.security.SecureRandom;
/**
* Class helping with the card duplication process. Depending on the device's role, only some of the methods are relevant.
*
* WORK IN PROGRESS, DO NOT USE YET
*/
public class CardDuplicator {
private byte[] secret;
/**
* Creates a CardDuplicator object. Regardless of the role of the device, this object must be kept and used for the
* entire duplication session. It cannot be reused for multiple sessions.
*/
public CardDuplicator() {
secret = new byte[32];
SecureRandom random = new SecureRandom();
random.nextBytes(secret);
}
private KeycardCommandSet preamble(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.setPairing(pairing);
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN(pin).checkOK();
return cmdSet;
}
/**
* Starts duplication session. Must be used on all cards taking part of in the duplication process.
*
* @param channel the card channel
* @param pairing the pairing info
* @param pin the card PIN
* @param deviceCount the number of devices which will be adding entropy for the key, including this one
*
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void startDuplication(CardChannel channel, Pairing pairing, String pin, int deviceCount) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
cmdSet.duplicateKeyStart(deviceCount, secret).checkOK();
}
/**
* Exports key. Must be used on the card designated as the source for the duplication.
*
* @param channel the card channel
* @param pairing the pairing info
* @param pin the card PIN
*
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] exportKey(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyExport().checkOK().getData();
}
/**
* Imports key. Must be used on all cards designated as the target for the duplication.
* @param channel the card channel
* @param pairing the pairing info
* @param pin the user PIN
* @param key the key to import
* @return the key UID
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] importKey(CardChannel channel, Pairing pairing, String pin, byte[] key) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyImport(key).checkOK().getData();
}
/**
* Adds entropy. Must be used on all cards taking part in the backup process. Each device taking part must use this
* exactly once, except for the device which started the backup.
*
* @param channel
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void addEntropy(CardChannel channel) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
}
}
@@ -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,17 @@
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.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
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 +19,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 +30,96 @@ 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 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;
public WalletAppletCommandSet(CardChannel apduChannel) {
public KeycardCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.secureChannel = new SecureChannelSession();
}
public void setSecureChannel(SecureChannelSession secureChannel) {
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);
this.secureChannel.generateSecret(extractPublicKeyFromSelect(resp.getData()));
this.secureChannel.reset();
}
return resp;
@@ -98,7 +128,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 +140,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(), 50000, 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,29 +238,15 @@ 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);
}
/**
@@ -276,31 +302,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 +338,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 +356,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 +412,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 +535,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.
*
@@ -531,12 +619,6 @@ public class WalletAppletCommandSet {
}
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");
}
return new ApplicationInfo(select).getSecureChannelPubKey();
}
}
@@ -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,130 @@
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 = tlv.readPrimitive(TinyBERTLV.TLV_INT);
s = 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");
}
}
/**
* 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 spongyCastleLoaded = false;
public static void addSpongyCastleProvider() {
if (!spongyCastleLoaded) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
spongyCastleLoaded = 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,132 @@
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
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");
}
}
/**
* 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,23 @@
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();
}
@@ -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();
}
+4 -2
View File
@@ -1,2 +1,4 @@
include ':lib'
include ':demo'
include 'lib'
include 'android'
include 'desktop'
include 'demo-android'