Compare commits

..
23 Commits
Author SHA1 Message Date
Bitgamma a4ff736d6e Keycard v2.2 (#15)
* add methods for the extended SIGN command

* add resetPinlessPath
2019-04-04 10:20:38 +03:00
Michele Balistreri cd7c4ba3bc Merge remote-tracking branch 'origin/master' 2019-03-27 19:04:14 +03:00
Michele Balistreri 152d7f8e34 update documentation 2019-03-27 19:03:09 +03:00
ligi ee41722527 Only ignore specific lint issue (#14)
while  f4dd1d1 fixes #13 it is very broadly deactivating lint trowing errors
This PR makes it just ignore the specific lint problem at hand
2019-03-27 18:38:39 +03:00
Michele Balistreri f4dd1d17cf closes #13 2019-03-27 13:08:02 +03:00
Michele Balistreri 425d085716 correct value for Ledger 2019-03-20 13:24:22 +03:00
Michele Balistreri b249bd75e6 make the iteration count for PBKDF2 configurable per-device 2019-03-20 13:18:24 +03:00
Michele Balistreri f603979cb5 fix sending commands longer than 64 bytes 2019-03-18 11:54:15 +03:00
Michele Balistreri db3eee0d76 make r and s unsigned 2019-03-14 12:56:32 +03:00
Michele Balistreri 99dbb24067 increase read timeout 2019-02-27 10:52:36 +03:00
Michele Balistreri f9df2be80f add implicit capabilities of non-initialized devices 2019-02-26 14:23:48 +03:00
Michele Balistreri 07cba4aa9d support devices without SecureChannel capability 2019-02-26 12:07:42 +03:00
Michele Balistreri 527efc7a4e add initial USB connector 2019-02-25 16:30:49 +03:00
Michele Balistreri 067204c7db update demo 2019-02-13 12:22:09 +03:00
Michele Balistreri 2df15f388c add capability parsing 2019-02-12 14:38:24 +03:00
Michele Balistreri 93d71ab70b keep track of used cards 2019-01-15 17:00:01 +03:00
Michele Balistreri 50fa93dd32 change the convenience constructor 2019-01-15 11:30:12 +03:00
Michele Balistreri bbe3693e4e implement card duplication 2019-01-14 18:23:52 +03:00
Michele Balistreri b5ff6d033e simplify PIN verification 2019-01-08 15:15:18 +03:00
Michele Balistreri 93ff092a14 add utility credentials change methods 2019-01-08 14:23:10 +03:00
Michele Balistreri f6f9a65be1 remove spongycastle dependency 2018-12-18 12:12:08 +03:00
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
21 changed files with 728 additions and 109 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ You can import the SDK in your Gradle or Maven project using [Jitpack.io](https:
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:android:2.0rc1'
implementation 'com.github.status-im.status-keycard-java:android:2.2.0'
}
```
@@ -23,6 +23,6 @@ dependencies {
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:2.0rc1'
implementation 'com.github.status-im.status-keycard-java:desktop:2.2.0'
}
```
+11 -2
View File
@@ -8,8 +8,17 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "2.0"
versionCode 201
versionName "2.0.1"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
}
@@ -24,7 +24,7 @@ public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback
private int loopSleepMS;
static {
Crypto.addSpongyCastleProvider();
Crypto.addBouncyCastleProvider();
}
/**
+4
View File
@@ -22,3 +22,7 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}
subprojects {
tasks.withType(Javadoc).all { enabled = false }
}
+12 -4
View File
@@ -6,8 +6,8 @@ android {
applicationId "im.status.keycard.demo"
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "2.0"
versionCode 201
versionName "2.0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -16,13 +16,21 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
lintConfig file("lint-config.xml")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
implementation project(':android')
+5
View File
@@ -0,0 +1,5 @@
<lint>
<issue id="InvalidPackage">
<ignore path="**/bcprov-jdk15on-*.jar"/>
</issue>
</lint>
@@ -9,7 +9,7 @@ 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;
import org.bouncycastle.util.encoders.Hex;
public class MainActivity extends AppCompatActivity {
@@ -54,20 +54,27 @@ public class MainActivity extends AppCompatActivity {
} else {
Log.i(TAG, "The card has no master key");
}
Log.i(TAG, String.format("Capabilities: %02X", info.getCapabilities()));
Log.i(TAG, "Has Secure Channel: " + info.hasSecureChannelCapability());
Log.i(TAG, "Has Key Management: " + info.hasKeyManagementCapability());
Log.i(TAG, "Has Credentials Management: " + info.hasCredentialsManagementCapability());
Log.i(TAG, "Has NDEF capability: " + info.hasNDEFCapability());
// In real projects, the pairing key should be saved and used for all new sessions.
cmdSet.autoPair("KeycardTest");
Pairing pairing = cmdSet.getPairing();
if (info.hasSecureChannelCapability()) {
// In real projects, the pairing key should be saved and used for all new sessions.
cmdSet.autoPair("KeycardTest");
Pairing pairing = cmdSet.getPairing();
// Never log the pairing key in a real application!
Log.i(TAG, "Pairing with card is done.");
Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
// 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();
// Opening a Secure Channel is needed for all other applet commands
cmdSet.autoOpenSecureChannel();
Log.i(TAG, "Secure channel opened. Getting applet status.");
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());
@@ -76,27 +83,31 @@ public class MainActivity extends AppCompatActivity {
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());
if (info.hasKeyManagementCapability()) {
// A mnemonic can be generated before PIN authentication. Generating a mnemonic does not create keys on the
// card. a subsequent loadKey step must be performed after PIN authentication. In this example we will only
// show how to convert the output of the card to a usable format but won't actually load the key
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(KeycardCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
// We need to set a wordlist if we plan using this object to derive the binary seed. If we just need the word
// indexes we can skip this step and call mnemonic.getIndexes() instead.
mnemonic.fetchBIP39EnglishWordlist();
// 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()));
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();
if (info.hasCredentialsManagementCapability()) {
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkAuthOK();
Log.i(TAG, "Pin Verified.");
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()) {
if (!status.hasMasterKey() && info.hasKeyManagementCapability()) {
cmdSet.generateKey();
//cmdSet.loadKey(mnemonic.toBIP32KeyPair());
}
@@ -127,14 +138,15 @@ public class MainActivity extends AppCompatActivity {
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.");
if (info.hasSecureChannelCapability()) {
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
// this would be a very bad idea to do.
cmdSet.unpairOthers();
cmdSet.autoUnpair();
Log.i(TAG, "Unpaired.");
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
+1
View File
@@ -3,6 +3,7 @@ apply plugin: 'maven'
dependencies {
compile project(':lib')
compile 'org.hid4java:hid4java:0.5.0'
}
task sourcesJar(type: Jar, dependsOn: classes) {
@@ -0,0 +1,169 @@
package im.status.keycard.desktop;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import org.hid4java.HidDevice;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class LedgerUSBChannel implements CardChannel {
private static final int HID_BUFFER_SIZE = 64;
private static final int LEDGER_DEFAULT_CHANNEL = 1;
private static final int TAG_APDU = 0x05;
private static final int READ_TIMEOUT = 20000;
private HidDevice hidDevice;
public LedgerUSBChannel(HidDevice hidDevice) {
this.hidDevice = hidDevice;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
ByteArrayOutputStream response = new ByteArrayOutputStream();
int offset = 0;
byte[] command = wrapCommandAPDU(cmd.serialize());
byte[] chunk = new byte[HID_BUFFER_SIZE];
while(offset != command.length) {
System.arraycopy(command, offset, chunk, 0, HID_BUFFER_SIZE);
if (hidDevice.write(chunk, HID_BUFFER_SIZE, (byte) 0x00) < 0) {
throw new IOException("Write failed");
}
offset += HID_BUFFER_SIZE;
}
byte[] responseData = null;
while ((responseData = unwrapResponseAPDU(response.toByteArray())) == null) {
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
throw new IOException("Read failed");
}
response.write(chunk, 0, HID_BUFFER_SIZE);
}
return new APDUResponse(responseData);
}
private byte[] unwrapResponseAPDU(byte[] data) throws IOException {
if ((data == null) || (data.length < 7 + 5)) {
return null;
}
int sequenceIdx = 0;
int offset = checkResponseHeader(data, 0, sequenceIdx);
int responseLength = ((data[offset++] & 0xff) << 8);
responseLength |= (data[offset++] & 0xff);
if (data.length < 7 + responseLength) {
return null;
}
ByteArrayOutputStream response = new ByteArrayOutputStream();
int blockSize = (responseLength > HID_BUFFER_SIZE - 7 ? HID_BUFFER_SIZE - 7 : responseLength);
response.write(data, offset, blockSize);
offset += blockSize;
while (response.size() != responseLength) {
sequenceIdx++;
if (offset == data.length) {
return null;
}
offset = checkResponseHeader(data, offset, sequenceIdx);
blockSize = (responseLength - response.size() > HID_BUFFER_SIZE - 5 ? HID_BUFFER_SIZE - 5 : responseLength - response.size());
if (blockSize > data.length - offset) {
return null;
}
response.write(data, offset, blockSize);
offset += blockSize;
}
return response.toByteArray();
}
private int checkResponseHeader(byte[] data, int offset, int sequenceIdx) throws IOException {
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL >> 8)) {
throw new IOException("Invalid channel");
}
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL & 0xff)) {
throw new IOException("Invalid channel");
}
if (data[offset++] != TAG_APDU) {
throw new IOException("Invalid tag");
}
if (data[offset++] != (sequenceIdx >> 8)) {
throw new IOException("Invalid sequence");
}
if (data[offset++] != (sequenceIdx & 0xff)) {
throw new IOException("Invalid sequence");
}
return offset;
}
private byte[] wrapCommandAPDU(byte[] command) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
int sequenceIdx = 0;
int offset = 0;
writeCommandHeader(output, sequenceIdx);
sequenceIdx++;
output.write(command.length >> 8);
output.write(command.length);
int blockSize = (command.length > (HID_BUFFER_SIZE - 7) ? (HID_BUFFER_SIZE - 7) : command.length);
output.write(command, offset, blockSize);
offset += blockSize;
while (offset != command.length) {
writeCommandHeader(output, sequenceIdx);
sequenceIdx++;
blockSize = ((command.length - offset) > (HID_BUFFER_SIZE - 5) ? (HID_BUFFER_SIZE - 5) : (command.length - offset));
output.write(command, offset, blockSize);
offset += blockSize;
}
if ((output.size() % HID_BUFFER_SIZE) != 0) {
byte[] padding = new byte[HID_BUFFER_SIZE - (output.size() % HID_BUFFER_SIZE)];
output.write(padding, 0, padding.length);
}
return output.toByteArray();
}
private void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx) {
output.write(LEDGER_DEFAULT_CHANNEL >> 8);
output.write(LEDGER_DEFAULT_CHANNEL);
output.write(TAG_APDU);
output.write(sequenceIdx >> 8);
output.write(sequenceIdx);
}
@Override
public boolean isConnected() {
return hidDevice.isOpen();
}
@Override
public int pairingPasswordPBKDF2IterationCount() {
return 10;
}
}
@@ -0,0 +1,71 @@
package im.status.keycard.desktop;
import im.status.keycard.globalplatform.Crypto;
import im.status.keycard.io.CardListener;
import org.hid4java.*;
import org.hid4java.event.HidServicesEvent;
public class LedgerUSBManager implements HidServicesListener {
static {
Crypto.addBouncyCastleProvider();
}
private static final int VID = 0x2c97;
private static final int PID = 0x0001;
private static final int SCAN_INTERVAL_MS = 500;
private static final int PAUSE_INTERVAL_MS = 5000;
private HidServices hidServices;
private CardListener listener;
public LedgerUSBManager(CardListener listener) {
this.listener = listener;
HidServicesSpecification hidServicesSpecification = new HidServicesSpecification();
hidServicesSpecification.setAutoShutdown(true);
hidServicesSpecification.setScanInterval(SCAN_INTERVAL_MS);
hidServicesSpecification.setPauseInterval(PAUSE_INTERVAL_MS);
hidServicesSpecification.setScanMode(ScanMode.SCAN_AT_FIXED_INTERVAL_WITH_PAUSE_AFTER_WRITE);
hidServices = HidManager.getHidServices(hidServicesSpecification);
hidServices.addHidServicesListener(this);
}
public void start() {
hidServices.start();
HidDevice hidDevice = hidServices.getHidDevice(VID, PID, null);
if (hidDevice != null) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
}
}
public void stop() {
hidServices.shutdown();
}
@Override
public void hidDeviceAttached(HidServicesEvent event) {
HidDevice hidDevice = event.getHidDevice();
if (hidDevice.isVidPidSerial(VID, PID, null)) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
}
}
@Override
public void hidDeviceDetached(HidServicesEvent event) {
hidFailure(event);
}
@Override
public void hidFailure(HidServicesEvent event) {
HidDevice hidDevice = event.getHidDevice();
if (hidDevice.isVidPidSerial(VID, PID, null)) {
listener.onDisconnected();
}
}
}
@@ -15,7 +15,7 @@ import java.io.IOException;
*/
public class PCSCCardChannel implements CardChannel {
static {
Crypto.addSpongyCastleProvider();
Crypto.addBouncyCastleProvider();
}
private javax.smartcardio.CardChannel cardChannel;
@@ -11,11 +11,20 @@ public class ApplicationInfo {
private short appVersion;
private byte freePairingSlots;
private byte[] keyUID;
private byte capabilities;
public static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
public static final byte TLV_PUB_KEY = (byte) 0x80;
public static final byte TLV_UID = (byte) 0x8F;
public static final byte TLV_KEY_UID = (byte) 0x8E;
public static final byte TLV_CAPABILITIES = (byte) 0x8D;
static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01;
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04;
static final byte CAPABILITY_NDEF = (byte) 0x08;
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF;
/**
* Constructs an object by parsing the TLV data.
@@ -32,6 +41,12 @@ public class ApplicationInfo {
if (topTag == TLV_PUB_KEY) {
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
initializedCard = false;
capabilities = CAPABILITY_CREDENTIALS_MANAGEMENT;
if (secureChannelPubKey.length > 0) {
capabilities |= CAPABILITY_SECURE_CHANNEL;
}
return;
}
@@ -41,6 +56,14 @@ public class ApplicationInfo {
appVersion = (short) tlv.readInt();
freePairingSlots = (byte) tlv.readInt();
keyUID = tlv.readPrimitive(TLV_KEY_UID);
if (tlv.readTag() != TinyBERTLV.END_OF_TLV) {
tlv.unreadLastTag();
capabilities = tlv.readPrimitive(TLV_CAPABILITIES)[0];
} else {
capabilities = CAPABILITIES_ALL;
}
initializedCard = true;
}
@@ -115,4 +138,29 @@ public class ApplicationInfo {
public byte[] getKeyUID() {
return keyUID;
}
/**
* Returns the capability descriptor for the device.
*
* @return the capability descriptor for the device.
*/
public byte getCapabilities() {
return capabilities;
}
public boolean hasSecureChannelCapability() {
return (capabilities & CAPABILITY_SECURE_CHANNEL) == CAPABILITY_SECURE_CHANNEL;
}
public boolean hasKeyManagementCapability() {
return (capabilities & CAPABILITY_KEY_MANAGEMENT) == CAPABILITY_KEY_MANAGEMENT;
}
public boolean hasCredentialsManagementCapability() {
return (capabilities & CAPABILITY_CREDENTIALS_MANAGEMENT) == CAPABILITY_CREDENTIALS_MANAGEMENT;
}
public boolean hasNDEFCapability() {
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
}
}
@@ -2,95 +2,140 @@ package im.status.keycard.applet;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.WrongPINException;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashSet;
/**
* Class helping with the card duplication process. Depending on the device's role, only some of the methods are relevant.
*
* WORK IN PROGRESS, DO NOT USE YET
* Class helping with the card duplication process. Depending on the client's role, only some of the methods are relevant.
*/
public class CardDuplicator {
private byte[] secret;
private KeycardCommandSet cmdSet;
private DuplicatorCallback cb;
private HashSet<byte[]> startedDuplication;
private HashSet<byte[]> addedEntropy;
private HashSet<byte[]> finishedDuplication;
/**
* Creates a CardDuplicator object. Regardless of the role of the device, this object must be kept and used for the
* Creates a CardDuplicator object. Regardless of the role of the client, this object must be kept and used for the
* entire duplication session. It cannot be reused for multiple sessions.
*
* @param cmdSet the CommandSet to use
* @param cb the callback object for backups. This is needed only on the client performing steps requiring pairing
* and authentication. Clients which only add entropy should pass null
*/
public CardDuplicator() {
secret = new byte[32];
public CardDuplicator(KeycardCommandSet cmdSet, DuplicatorCallback cb) {
this.cmdSet = cmdSet;
this.cb = cb;
this.startedDuplication = new HashSet<>();
this.addedEntropy = new HashSet<>();
this.finishedDuplication = new HashSet<>();
this.secret = new byte[32];
SecureRandom random = new SecureRandom();
random.nextBytes(secret);
random.nextBytes(this.secret);
}
private KeycardCommandSet preamble(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
/**
* Creates a CardDuplicator object. Only suitable for clients performing the role of adding entropy.
*
* @param channel the APDU channel
*/
public CardDuplicator(CardChannel channel) {
this(new KeycardCommandSet(channel), null);
}
private ApplicationInfo selectAndCheck(HashSet<byte[]> processed) throws APDUException, IOException {
ApplicationInfo appInfo = new ApplicationInfo(cmdSet.select().checkOK().getData());
if (!processed.add(appInfo.getInstanceUID())) {
throw new IllegalStateException("The requested action has been already performed on this card");
}
return appInfo;
}
private void preamble(HashSet<byte[]> processed) throws IOException, APDUException {
ApplicationInfo appInfo = selectAndCheck(processed);
Pairing pairing = cb.getPairing(appInfo);
if (pairing == null) {
throw new APDUException("The given card is not paired");
}
cmdSet.setPairing(pairing);
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN(pin).checkOK();
return cmdSet;
ApplicationStatus appStatus = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
int remainingAttempts = appStatus.getPINRetryCount();
while(remainingAttempts > 0) {
try {
cmdSet.verifyPIN(cb.getPIN(appInfo, remainingAttempts)).checkAuthOK();
break;
} catch(WrongPINException e) {
remainingAttempts = e.getRetryAttempts();
}
}
if (remainingAttempts <= 0) {
throw new APDUException("Card blocked");
}
}
/**
* Starts duplication session. Must be used on all cards taking part of in the duplication process.
*
* @param 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
* @param clientCount the number of clients which will be adding entropy for the key, including this one
*
* @throws IOException communication error
* @throws APDUException unexpected card response
* @throws IllegalStateException this card has already been used
*/
public void startDuplication(CardChannel channel, Pairing pairing, String pin, int deviceCount) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
cmdSet.duplicateKeyStart(deviceCount, secret).checkOK();
public void startDuplication(int clientCount) throws IOException, APDUException, IllegalStateException {
preamble(startedDuplication);
cmdSet.duplicateKeyStart(clientCount, secret).checkOK();
}
/**
* Exports key. Must be used on the card designated as the source for the duplication.
*
* @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);
public byte[] exportKey() throws IOException, APDUException, IllegalStateException {
preamble(finishedDuplication);
return cmdSet.duplicateKeyExport().checkOK().getData();
}
/**
* Imports key. Must be used on all cards designated as the target for the duplication.
* @param 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
* @throws IllegalStateException this card has already been used
*/
public byte[] importKey(CardChannel channel, Pairing pairing, String pin, byte[] key) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
public byte[] importKey(byte[] key) throws IOException, APDUException, IllegalStateException {
preamble(finishedDuplication);
return cmdSet.duplicateKeyImport(key).checkOK().getData();
}
/**
* Adds entropy. Must be used on all cards taking part in the backup process. Each device taking part must use this
* exactly once, except for the device which started the backup.
* Adds entropy. Must be used on all cards taking part in the backup process. Each client taking part must use this
* exactly once, except for the client which started the backup.
*
* @param channel
* @throws IOException communication error
* @throws APDUException unexpected card response
* @throws IllegalStateException this card has already been used
*/
public void addEntropy(CardChannel channel) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
public void addEntropy() throws IOException, APDUException, IllegalStateException {
selectAndCheck(addedEntropy);
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
}
}
@@ -0,0 +1,24 @@
package im.status.keycard.applet;
/**
* Callback interface for duplication procedure.
*/
public interface DuplicatorCallback {
/**
* Must return the pairing for the current card, represented by the applicationInfo parameter. If no pairing
* could be found, null must be returned.
*
* @param applicationInfo the application info template of the currently inserted card
* @return the pairing info or null
*/
Pairing getPairing(ApplicationInfo applicationInfo);
/**
* Must return the PIN for the current card. This method can prompt the user or return a cached value.
*
* @param applicationInfo the application info template of the currently inserted card
* @param remainingAttempts the number of remaining PIN attempts
* @return the PIN
*/
String getPIN(ApplicationInfo applicationInfo, int remainingAttempts);
}
@@ -2,21 +2,39 @@ 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("53746174757357616C6C6574");
public static final byte[] PACKAGE_AID = Hex.decode("A0000008040001");
public static final byte[] KEYCARD_AID = Hex.decode("53746174757357616C6C6574417070");
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("53746174757357616C6C65744E4643");
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 Keycard applet. Since multiple instances this is a method instead of a constant.
* Soon a method taking an additional instance index will be added.
* 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 KEYCARD_AID;
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;
}
}
@@ -1,9 +1,6 @@
package im.status.keycard.applet;
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 im.status.keycard.io.*;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
@@ -36,6 +33,10 @@ public class KeycardCommandSet {
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
public static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
public static final byte CHANGE_PIN_P1_PUK = 0x01;
public static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02;
public static final byte GET_STATUS_P1_APPLICATION = 0x00;
public static final byte GET_STATUS_P1_KEY_PATH = 0x01;
@@ -52,6 +53,11 @@ public class KeycardCommandSet {
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
static final byte SIGN_P1_CURRENT_KEY = 0x00;
static final byte SIGN_P1_DERIVE = 0x01;
static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte SIGN_P1_PINLESS = 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;
@@ -69,12 +75,31 @@ public class KeycardCommandSet {
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
private ApplicationInfo info;
/**
* Creates a KeycardCommandSet using the given APDU Channel
* @param apduChannel APDU channel
*/
public KeycardCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.secureChannel = new SecureChannelSession();
}
/**
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
* command has been sent using this command set.
*
* @return the application info object
*/
public ApplicationInfo getApplicationInfo() {
return info;
}
/**
* Set the SecureChannel object
* @param secureChannel secure channel
*/
protected void setSecureChannel(SecureChannelSession secureChannel) {
this.secureChannel = secureChannel;
}
@@ -95,19 +120,36 @@ public class KeycardCommandSet {
}
/**
* 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.
* 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, Identifiers.getKeycardInstanceAID());
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) {
this.secureChannel.generateSecret(extractPublicKeyFromSelect(resp.getData()));
this.secureChannel.reset();
info = new ApplicationInfo(resp.getData());
if (info.hasSecureChannelCapability()) {
this.secureChannel.generateSecret(info.getSecureChannelPubKey());
this.secureChannel.reset();
}
}
return resp;
@@ -144,7 +186,7 @@ public class KeycardCommandSet {
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", "BC");
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), 50000, 32 * 8);
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), apduChannel.pairingPasswordPBKDF2IterationCount(), 32 * 8);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
@@ -241,7 +283,7 @@ public class KeycardCommandSet {
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pin the pin
* @param pin the PIN
* @return the raw card response
* @throws IOException communication error
*/
@@ -250,6 +292,40 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, verifyPIN);
}
/**
* Sends a CHANGE PIN APDU to change the user PIN.
*
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(String pin) throws IOException {
return changePIN(CHANGE_PIN_P1_USER_PIN, pin.getBytes());
}
/**
* Sends a CHANGE PIN APDU to change the PUK.
*
* @param puk the new PUK
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePUK(String puk) throws IOException {
return changePIN(CHANGE_PIN_P1_PUK, puk.getBytes());
}
/**
* Sends a CHANGE PIN APDU to change the pairing password. This does not break existing pairings, but new pairings
* will be made using the new password.
*
* @param pairingPassword the new pairing password
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePairingPassword(String pairingPassword) throws IOException {
return changePIN(CHANGE_PIN_P1_PAIRING_SECRET, pairingPasswordToSecret(pairingPassword));
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
@@ -462,14 +538,58 @@ public class KeycardCommandSet {
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_CURRENT_KEY);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The key used to sign is given
* as a parameter.
*
* @param hash the hash to sign
* @params path the path of the key to use
* @param makeCurrent ture if the key used to sign should become the current key, false otherwise
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signWithPath(byte[] hash, String path, boolean makeCurrent) throws IOException {
KeyPath keyPath = new KeyPath(path);
byte[] pathData = keyPath.getData();
byte[] data = Arrays.copyOf(hash, hash.length + pathData.length);
System.arraycopy(pathData, 0, data, hash.length, pathData.length);
return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE));
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The pinless path will be used
* to sign. This command is the only variant of SIGN which can also be executed without a Secure Channel.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signPinless(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_PINLESS);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
* a derivation path.
*
* @param p1 the p1 parameter
* @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);
public APDUResponse sign(byte[] data, int p1) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x00, data);
return secureChannel.transmit(apduChannel, sign);
}
@@ -510,6 +630,33 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The path must be absolute, that is starting from the master key.
* @param path the path. Must be an absolute path (i.e: starting from the master key)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(String path) throws IOException {
KeyPath keyPath = new KeyPath(path);
if (keyPath.getSource() != DERIVE_P1_SOURCE_MASTER) {
throw new IllegalArgumentException("Only absolute paths can be set as PINLESS path");
}
return setPinlessPath(keyPath.getData());
}
/**
* Sends an empty SET PINLESS PATH APDU, resetting it. After this command the card does not have a PINless path until
* a new one is set.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse resetPinlessPath() throws IOException {
return setPinlessPath(new byte[]{});
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
@@ -517,7 +664,7 @@ public class KeycardCommandSet {
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
public APDUResponse setPinlessPath(byte[] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
@@ -605,8 +752,4 @@ public class KeycardCommandSet {
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
private byte[] extractPublicKeyFromSelect(byte[] select) {
return new ApplicationInfo(select).getSecureChannelPubKey();
}
}
@@ -43,8 +43,8 @@ public class RecoverableSignature {
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);
r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
recId = -1;
@@ -62,6 +62,14 @@ public class RecoverableSignature {
}
}
private byte[] toUInt(byte[] signedInt) {
if (signedInt[0] == 0) {
return Arrays.copyOfRange(signedInt, 1, signedInt.length);
} else {
return signedInt;
}
}
/**
* The public key associated to this signature.
*
@@ -21,13 +21,13 @@ public class Crypto {
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
private static boolean spongyCastleLoaded = false;
private static boolean bouncyCastleLoaded = false;
public static void addSpongyCastleProvider() {
if (!spongyCastleLoaded) {
public static void addBouncyCastleProvider() {
if (!bouncyCastleLoaded) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
spongyCastleLoaded = true;
bouncyCastleLoaded = true;
}
}
@@ -10,6 +10,7 @@ public class APDUResponse {
public static final int SW_CARD_LOCKED = 0x6283;
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
public static final int SW_WRONG_PIN_MASK = 0x63C0;
private byte[] apdu;
private byte[] data;
@@ -87,6 +88,20 @@ public class APDUResponse {
}
}
/**
* Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
*
* @throws WrongPINException wrong PIN
* @throws APDUException unexpected response
*/
public APDUResponse checkAuthOK() throws WrongPINException, APDUException {
if ((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
throw new WrongPINException(sw2 & 0x0F);
} else {
return checkOK();
}
}
/**
* Returns the data field of this APDU.
*
@@ -20,4 +20,16 @@ public interface CardChannel {
* @return true if connected, false otherwise
*/
boolean isConnected();
/**
* Returns the iteration count for deriving the pairing key from the pairing password. The default is 50000 and is
* should only be changed for devices where the PBKDF2 is calculated on-board and the resource do not permit a
* high iteration count. If a lower count is used other security mechanism should be used to prevent brute force
* attacks.
*
* @return the iteration count
*/
default int pairingPasswordPBKDF2IterationCount() {
return 50000;
}
}
@@ -0,0 +1,27 @@
package im.status.keycard.io;
/**
* Exception thrown when checking PIN/PUK
*/
public class WrongPINException extends APDUException {
private int retryAttempts;
/**
* Construct an exception with the given number of retry attempts.
*
* @param retryAttempts the number of retry attempts
*/
public WrongPINException(int retryAttempts) {
super("Wrong PIN");
this.retryAttempts = retryAttempts;
}
/**
* Returns the number of available retry attempts.
*
* @return the number of retry attempts
*/
public int getRetryAttempts() {
return retryAttempts;
}
}