Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ff736d6e | ||
|
|
cd7c4ba3bc | ||
|
|
152d7f8e34 | ||
|
|
ee41722527 | ||
|
|
f4dd1d17cf | ||
|
|
425d085716 | ||
|
|
b249bd75e6 | ||
|
|
f603979cb5 | ||
|
|
db3eee0d76 | ||
|
|
99dbb24067 | ||
|
|
f9df2be80f | ||
|
|
07cba4aa9d | ||
|
|
527efc7a4e | ||
|
|
067204c7db | ||
|
|
2df15f388c | ||
|
|
93d71ab70b | ||
|
|
50fa93dd32 | ||
|
|
bbe3693e4e | ||
|
|
b5ff6d033e | ||
|
|
93ff092a14 | ||
|
|
f6f9a65be1 |
@@ -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.0.0'
|
||||
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.0.0'
|
||||
implementation 'com.github.status-im.status-keycard-java:desktop:2.2.0'
|
||||
}
|
||||
```
|
||||
+11
-2
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,3 +22,7 @@ allprojects {
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.withType(Javadoc).all { enabled = false }
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -117,9 +142,14 @@ public class KeycardCommandSet {
|
||||
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;
|
||||
@@ -156,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?");
|
||||
@@ -253,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
|
||||
*/
|
||||
@@ -262,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.
|
||||
@@ -474,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);
|
||||
}
|
||||
|
||||
@@ -522,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.
|
||||
*
|
||||
@@ -529,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);
|
||||
}
|
||||
@@ -617,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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user