Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a20f8f601 | ||
|
|
3f8966f1a8 | ||
|
|
3acea10750 | ||
|
|
8cb43e6717 | ||
|
|
6bf8da8374 | ||
|
|
536bad2671 | ||
|
|
b1be261ea1 | ||
|
|
4a69788473 | ||
|
|
eed0b09fed | ||
|
|
7ce0136b1d | ||
|
|
2865d2a08a | ||
|
|
3e5bb577d7 | ||
|
|
e882e39105 | ||
|
|
15a2c43a70 | ||
|
|
205f150705 | ||
|
|
a4ff736d6e | ||
|
|
cd7c4ba3bc | ||
|
|
152d7f8e34 | ||
|
|
ee41722527 | ||
|
|
f4dd1d17cf | ||
|
|
425d085716 | ||
|
|
b249bd75e6 | ||
|
|
f603979cb5 | ||
|
|
db3eee0d76 | ||
|
|
99dbb24067 | ||
|
|
f9df2be80f | ||
|
|
07cba4aa9d | ||
|
|
527efc7a4e | ||
|
|
067204c7db | ||
|
|
2df15f388c | ||
|
|
93d71ab70b | ||
|
|
50fa93dd32 | ||
|
|
bbe3693e4e | ||
|
|
b5ff6d033e | ||
|
|
93ff092a14 | ||
|
|
f6f9a65be1 | ||
|
|
0d16541875 | ||
|
|
171220bd79 |
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -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
@@ -8,8 +8,17 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 2
|
||||
versionName "2.0"
|
||||
versionCode 300
|
||||
versionName "3.0.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,169 @@
|
||||
package im.status.keycard.android;
|
||||
|
||||
import android.bluetooth.*;
|
||||
import android.content.Context;
|
||||
import im.status.keycard.io.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LedgerBLEChannel implements CardChannel {
|
||||
final public static UUID LEDGER_UUID = UUID.fromString("13D63400-2C97-0004-0000-4C6564676572");
|
||||
final public static UUID LEDGER_REQ_UUID = UUID.fromString("13D63400-2C97-0004-0002-4C6564676572");
|
||||
final public static UUID LEDGER_RSP_UUID = UUID.fromString("13D63400-2C97-0004-0001-4C6564676572");
|
||||
|
||||
final private static int BLE_WRITE_FAILED = -1;
|
||||
final private static int BLE_WRITE_STARTED = 0;
|
||||
final private static int BLE_WRITE_FINISHED = 1;
|
||||
|
||||
final private static int BLE_TIMEOUT = 2000;
|
||||
|
||||
|
||||
final private BluetoothGatt bluetoothGatt;
|
||||
private BluetoothGattCharacteristic reqChar;
|
||||
private boolean connected;
|
||||
private int mtuSize;
|
||||
private int writeStatus;
|
||||
private LinkedBlockingQueue<byte[]> readQueue;
|
||||
|
||||
public LedgerBLEChannel(Context context, BluetoothDevice device, CardListener listener) {
|
||||
this.connected = false;
|
||||
this.mtuSize = 20;
|
||||
this.readQueue = new LinkedBlockingQueue<>();
|
||||
this.writeStatus = BLE_WRITE_FINISHED;
|
||||
final CardChannel channel = this;
|
||||
|
||||
this.bluetoothGatt = device.connectGatt(context, false, new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
if (connected == (newState == BluetoothProfile.STATE_CONNECTED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = newState == BluetoothProfile.STATE_CONNECTED;
|
||||
|
||||
if (connected) {
|
||||
bluetoothGatt.discoverServices();
|
||||
} else {
|
||||
(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onDisconnected();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService service = bluetoothGatt.getService(LEDGER_UUID);
|
||||
|
||||
if (service == null) {
|
||||
bluetoothGatt.disconnect();
|
||||
connected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
reqChar = service.getCharacteristic(LEDGER_REQ_UUID);
|
||||
BluetoothGattCharacteristic rsp = service.getCharacteristic(LEDGER_RSP_UUID);
|
||||
bluetoothGatt.setCharacteristicNotification(rsp, true);
|
||||
|
||||
BluetoothGattDescriptor rspDesc = rsp.getDescriptors().get(0);
|
||||
rspDesc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.writeDescriptor(rspDesc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
writeStatus = status == BluetoothGatt.GATT_SUCCESS ? BLE_WRITE_FINISHED : BLE_WRITE_FAILED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
reqChar.setValue(new byte[] {0x08, 0x00, 0x00, 0x00, 0x00});
|
||||
bluetoothGatt.writeCharacteristic(reqChar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
byte[] rsp = characteristic.getValue();
|
||||
|
||||
if (rsp[0] == 0x08) {
|
||||
mtuSize = rsp[5];
|
||||
(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onConnected(channel);
|
||||
}
|
||||
}).start();
|
||||
return;
|
||||
}
|
||||
|
||||
readQueue.offer(rsp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public APDUResponse send(APDUCommand cmd) throws IOException {
|
||||
return LedgerUtil.send(cmd, mtuSize, false, new LedgerUtil.Callback() {
|
||||
@Override
|
||||
public void write(byte[] chunk) throws IOException {
|
||||
writeStatus = BLE_WRITE_STARTED;
|
||||
reqChar.setValue(chunk);
|
||||
bluetoothGatt.writeCharacteristic(reqChar);
|
||||
|
||||
long timeout = 0;
|
||||
while(writeStatus == BLE_WRITE_STARTED || timeout >= BLE_TIMEOUT) {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
timeout += 10;
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("write interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
if (writeStatus != BLE_WRITE_FINISHED) {
|
||||
throw new IOException("write operation failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(byte[] chunk) throws IOException {
|
||||
try {
|
||||
byte[] data = readQueue.poll(BLE_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
|
||||
if (data == null) {
|
||||
throw new IOException("read timeout");
|
||||
}
|
||||
|
||||
System.arraycopy(data, 0, chunk, 0, Math.min(data.length, chunk.length));
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("read timeout");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pairingPasswordPBKDF2IterationCount() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
bluetoothGatt.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
close();
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package im.status.keycard.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.*;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import im.status.keycard.globalplatform.Crypto;
|
||||
import im.status.keycard.io.CardListener;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class LedgerBLEManager {
|
||||
private static final int REQUEST_ENABLE_BT = 1;
|
||||
|
||||
final private BluetoothAdapter bluetoothAdapter;
|
||||
final private Activity activity;
|
||||
private CardListener cardListener;
|
||||
|
||||
static {
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
public LedgerBLEManager(Activity context) {
|
||||
this.activity = context;
|
||||
final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
this.bluetoothAdapter = bluetoothManager.getAdapter();
|
||||
}
|
||||
|
||||
public void ensureBLEEnabled() {
|
||||
if (!bluetoothAdapter.isEnabled()) {
|
||||
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
|
||||
}
|
||||
}
|
||||
|
||||
public void startScan(BluetoothAdapter.LeScanCallback cb) {
|
||||
bluetoothAdapter.startLeScan(new UUID[] { LedgerBLEChannel.LEDGER_UUID}, cb);
|
||||
}
|
||||
|
||||
public void stopScan(BluetoothAdapter.LeScanCallback cb) {
|
||||
bluetoothAdapter.stopLeScan(cb);
|
||||
}
|
||||
|
||||
public void connectDevice(BluetoothDevice device) {
|
||||
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
|
||||
final IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
activity.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final BluetoothDevice d = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
|
||||
|
||||
if (!d.getAddress().equals(device.getAddress())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
activity.unregisterReceiver(this);
|
||||
// connect/disconnect to make bond permanent
|
||||
device.connectGatt(activity, false, new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
if (newState == BluetoothGatt.STATE_CONNECTED) {
|
||||
gatt.disconnect();
|
||||
onConnected(device);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, filter);
|
||||
|
||||
device.createBond();
|
||||
} else {
|
||||
onConnected(device);
|
||||
}
|
||||
}
|
||||
|
||||
private void onConnected(BluetoothDevice device) {
|
||||
if (cardListener != null) {
|
||||
new LedgerBLEChannel(activity, device, cardListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCardListener(CardListener cardListener) {
|
||||
this.cardListener = cardListener;
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,8 @@ public class NFCCardChannel implements CardChannel {
|
||||
public boolean isConnected() {
|
||||
return this.isoDep.isConnected();
|
||||
}
|
||||
|
||||
public int pairingPasswordPBKDF2IterationCount() {
|
||||
return 50000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 300
|
||||
versionName "3.0.0"
|
||||
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>
|
||||
@@ -1,15 +1,18 @@
|
||||
package im.status.keycard.app;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import im.status.keycard.android.LedgerBLEManager;
|
||||
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;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@@ -17,6 +20,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private NfcAdapter nfcAdapter;
|
||||
private NFCCardManager cardManager;
|
||||
//private LedgerBLEManager cardManager;
|
||||
//private boolean connected;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -24,7 +29,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
setContentView(R.layout.activity_main);
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
cardManager = new NFCCardManager();
|
||||
|
||||
//cardManager = new LedgerBLEManager(this);
|
||||
cardManager.setCardListener(new CardListener() {
|
||||
@Override
|
||||
public void onConnected(CardChannel cardChannel) {
|
||||
@@ -54,20 +59,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 +88,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 +143,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());
|
||||
}
|
||||
@@ -147,6 +164,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
cardManager.start();
|
||||
/*connected = false;
|
||||
cardManager.startScan(new BluetoothAdapter.LeScanCallback() {
|
||||
@Override
|
||||
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
|
||||
if (connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
cardManager.stopScan(this);
|
||||
cardManager.connectDevice(device);
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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,49 @@
|
||||
package im.status.keycard.desktop;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
import im.status.keycard.io.LedgerUtil;
|
||||
import org.hid4java.HidDevice;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class LedgerUSBChannel implements CardChannel {
|
||||
private static final int HID_BUFFER_SIZE = 64;
|
||||
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 {
|
||||
return LedgerUtil.send(cmd, HID_BUFFER_SIZE, true, new LedgerUtil.Callback() {
|
||||
@Override
|
||||
public void write(byte[] chunk) throws IOException {
|
||||
if (hidDevice.write(chunk, chunk.length, (byte) 0x00) < 0) {
|
||||
throw new IOException("Write failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(byte[] chunk) throws IOException {
|
||||
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
|
||||
throw new IOException("Read failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return hidDevice.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pairingPasswordPBKDF2IterationCount() {
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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[] PIDS = { 0x0001, 0x0004 };
|
||||
|
||||
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();
|
||||
|
||||
for (int pid : PIDS) {
|
||||
HidDevice hidDevice = hidServices.getHidDevice(VID, pid, null);
|
||||
|
||||
if (hidDevice != null) {
|
||||
listener.onConnected(new LedgerUSBChannel(hidDevice));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
hidServices.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceAttached(HidServicesEvent event) {
|
||||
HidDevice hidDevice = event.getHidDevice();
|
||||
|
||||
if (isLedger(hidDevice)) {
|
||||
listener.onConnected(new LedgerUSBChannel(hidDevice));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceDetached(HidServicesEvent event) {
|
||||
hidFailure(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidFailure(HidServicesEvent event) {
|
||||
if (isLedger(event.getHidDevice())) {
|
||||
listener.onDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLedger(HidDevice hidDevice) {
|
||||
for (int pid : PIDS) {
|
||||
if (hidDevice.isVidPidSerial(VID, pid, null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import java.io.IOException;
|
||||
*/
|
||||
public class PCSCCardChannel implements CardChannel {
|
||||
static {
|
||||
Crypto.addSpongyCastleProvider();
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
private javax.smartcardio.CardChannel cardChannel;
|
||||
@@ -47,4 +47,8 @@ public class PCSCCardChannel implements CardChannel {
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int pairingPasswordPBKDF2IterationCount() {
|
||||
return 50000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -95,6 +118,14 @@ public class ApplicationInfo {
|
||||
* @return the string representation of the application version
|
||||
*/
|
||||
public String getAppVersionString() {
|
||||
return getAppVersionString(appVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* A formatted application version.
|
||||
* @return the string representation of the application version
|
||||
*/
|
||||
static String getAppVersionString(short appVersion) {
|
||||
return (appVersion >> 8) + "." + (appVersion & 0xff);
|
||||
}
|
||||
|
||||
@@ -115,4 +146,49 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Secure Channel capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasSecureChannelCapability() {
|
||||
return (capabilities & CAPABILITY_SECURE_CHANNEL) == CAPABILITY_SECURE_CHANNEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Key Management capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasKeyManagementCapability() {
|
||||
return (capabilities & CAPABILITY_KEY_MANAGEMENT) == CAPABILITY_KEY_MANAGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Credentials Management capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasCredentialsManagementCapability() {
|
||||
return (capabilities & CAPABILITY_CREDENTIALS_MANAGEMENT) == CAPABILITY_CREDENTIALS_MANAGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the NDEF capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasNDEFCapability() {
|
||||
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,12 +171,13 @@ public class BIP32KeyPair {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key as an Ethereum address.
|
||||
*
|
||||
* @return the Ethereum address
|
||||
*/
|
||||
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);
|
||||
return Ethereum.toEthereumAddress(publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
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,62 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
/**
|
||||
* Parses the response from a SELECT command sent to the Cash applet.
|
||||
*/
|
||||
public class CashApplicationInfo {
|
||||
public static final byte TLV_PUB_DATA = (byte) 0x82;
|
||||
|
||||
private byte[] pubKey;
|
||||
private short appVersion;
|
||||
private byte[] pubData;
|
||||
|
||||
/**
|
||||
* 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 CashApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
|
||||
tlv.enterConstructed(ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE);
|
||||
pubKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
|
||||
appVersion = (short) tlv.readInt();
|
||||
pubData = tlv.readPrimitive(TLV_PUB_DATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* The public key of the wallet.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getPubKey() {
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ApplicationInfo.getAppVersionString(appVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* The public data of the cash applet.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getPubData() {
|
||||
return pubData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import im.status.keycard.io.APDUCommand;
|
||||
import im.status.keycard.io.APDUResponse;
|
||||
import im.status.keycard.io.CardChannel;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Command set for the Cash applet.
|
||||
*/
|
||||
public class CashCommandSet {
|
||||
private final CardChannel apduChannel;
|
||||
|
||||
/**
|
||||
* Creates a CashCommandSet using the given APDU Channel
|
||||
* @param apduChannel APDU channel
|
||||
*/
|
||||
public CashCommandSet(CardChannel apduChannel) {
|
||||
this.apduChannel = apduChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a Cash 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.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse select() throws IOException {
|
||||
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.CASH_INSTANCE_AID);
|
||||
return apduChannel.send(selectApplet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = new APDUCommand(0x80, KeycardCommandSet.INS_SIGN, 0x00, 0x00, data);
|
||||
return apduChannel.send(sign);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.status.keycard.applet;
|
||||
|
||||
import org.bouncycastle.crypto.digests.KeccakDigest;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Ethereum {
|
||||
private Ethereum() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] toEthereumAddress(byte[] publicKey) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,42 @@ 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");
|
||||
|
||||
public static final byte[] CASH_AID = Hex.decode("A000000804000103");
|
||||
public static final byte[] CASH_INSTANCE_AID = Hex.decode("A00000080400010301");
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -31,10 +28,15 @@ public class KeycardCommandSet {
|
||||
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;
|
||||
static final byte INS_GET_DATA = (byte) 0xCA;
|
||||
static final byte INS_STORE_DATA = (byte) 0xE2;
|
||||
|
||||
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 +54,15 @@ 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 byte STORE_DATA_P1_PUBLIC = 0x00;
|
||||
public static final byte STORE_DATA_P1_NDEF = 0x01;
|
||||
public static final byte STORE_DATA_P1_CASH = 0x02;
|
||||
|
||||
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 +80,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 +125,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 +191,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?");
|
||||
@@ -225,23 +272,11 @@ public class KeycardCommandSet {
|
||||
return secureChannel.transmit(apduChannel, getStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SET NDEF APDU.
|
||||
*
|
||||
* @param ndef the data field of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse setNDEF(byte[] ndef) throws IOException {
|
||||
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
|
||||
return secureChannel.transmit(apduChannel, setNDEF);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pin the pin
|
||||
* @param pin the PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
@@ -250,6 +285,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.
|
||||
@@ -411,65 +480,57 @@ public class KeycardCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
|
||||
*
|
||||
* @param entropyCount the number of entropy pieces to expect, including the one in this APDU
|
||||
* @param firstEntropy a random 32-byte number
|
||||
* @param hash the hash to sign
|
||||
* @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);
|
||||
public APDUResponse sign(byte[] hash) throws IOException {
|
||||
return sign(hash, SIGN_P1_CURRENT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 entropy a random 32-byte number
|
||||
* @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 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);
|
||||
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 DUPLICATE KEY APDU. The P1 is set to 02. This exports the encrypted master key including chaining code.
|
||||
* 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 duplicateKeyExport() throws IOException {
|
||||
APDUCommand duplicateKeyExport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_EXPORT, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyExport);
|
||||
public APDUResponse signPinless(byte[] hash) throws IOException {
|
||||
return sign(hash, SIGN_P1_PINLESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 +571,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 +605,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);
|
||||
}
|
||||
@@ -576,6 +664,55 @@ public class KeycardCommandSet {
|
||||
return secureChannel.transmit(apduChannel, exportKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET DATA APDU.
|
||||
*
|
||||
* @param dataType the type of data to be stored
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse getData(byte dataType) throws IOException {
|
||||
APDUCommand getData = secureChannel.protectedCommand(0x80, INS_GET_DATA, dataType, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, getData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a STORE DATA APDU for NDEF.
|
||||
*
|
||||
* @param ndef the data field of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse setNDEF(byte[] ndef) throws IOException {
|
||||
if ((info.getAppVersion() >> 8) > 2) {
|
||||
if ((ndef.length - 2) != ((ndef[0] << 8) | ndef[1])) {
|
||||
byte[] tmp = new byte[ndef.length + 2];
|
||||
tmp[0] = (byte) (ndef.length >> 8);
|
||||
tmp[1] = (byte) (ndef.length & 0xff);
|
||||
System.arraycopy(ndef, 0, tmp, 2, ndef.length);
|
||||
ndef = tmp;
|
||||
}
|
||||
|
||||
return storeData(ndef, STORE_DATA_P1_NDEF);
|
||||
} else {
|
||||
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
|
||||
return secureChannel.transmit(apduChannel, setNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a STORE DATA APDU.
|
||||
*
|
||||
* @param data the data field of the APDU
|
||||
* @param dataType the type of data to be stored
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse storeData(byte[] data, byte dataType) throws IOException {
|
||||
APDUCommand storeData = secureChannel.protectedCommand(0x80, INS_STORE_DATA, dataType, 0, data);
|
||||
return secureChannel.transmit(apduChannel, storeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
@@ -605,8 +742,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,28 +41,11 @@ public class Mnemonic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the official BIP39 english wordlist from GitHub.
|
||||
* Returns the official BIP39 english wordlist as fetched from https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt on 23 Oct 2019.
|
||||
*
|
||||
* @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);
|
||||
public void fetchBIP39EnglishWordlist() {
|
||||
this.wordlist = MnemonicEnglishDictionary.words;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
@@ -97,6 +105,15 @@ public class RecoverableSignature {
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Ethereum address of the signing key
|
||||
*
|
||||
* @return ethereum address of the signing key
|
||||
*/
|
||||
public byte[] getEthereumAddress() {
|
||||
return Ethereum.toEthereumAddress(publicKey);
|
||||
}
|
||||
|
||||
private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) {
|
||||
BigInteger n = CURVE.getN();
|
||||
BigInteger i = BigInteger.valueOf((long) recId / 2);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,21 @@ public class Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] ecb3des(byte[] key, byte[] data) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("DESede/ECB/NoPadding", "BC");
|
||||
SecretKeySpec keyDes = new SecretKeySpec(resizeKey24(key), "DES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyDes);
|
||||
return cipher.doFinal(data);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("Could not encrypt data", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] kcv3des(byte[] key) {
|
||||
return Arrays.copyOf(ecb3des(key, NullBytes8), 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 3DES MAC for SCP02 communication
|
||||
*
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GlobalPlatformCommandSet {
|
||||
static final byte INS_DELETE = (byte) 0xE4;
|
||||
static final byte INS_INSTALL = (byte) 0xE6;
|
||||
static final byte INS_LOAD = (byte) 0xE8;
|
||||
static final byte INS_PUT_KEY = (byte) 0xD8;
|
||||
|
||||
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
|
||||
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
|
||||
@@ -37,7 +38,10 @@ public class GlobalPlatformCommandSet {
|
||||
private SCP02Keys cardKeys;
|
||||
private Session session;
|
||||
|
||||
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
|
||||
private final byte[] gpDefaultKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
|
||||
private final SCP02Keys gpDefaultKeys = new SCP02Keys(gpDefaultKey, gpDefaultKey, gpDefaultKey);
|
||||
|
||||
private final byte[] developmentKey = Hex.decode("c212e073ff8b4bbfaff4de8ab655221f");
|
||||
|
||||
/**
|
||||
* Constructs a new command set with the given CardChannel.
|
||||
@@ -46,7 +50,26 @@ public class GlobalPlatformCommandSet {
|
||||
*/
|
||||
public GlobalPlatformCommandSet(CardChannel apduChannel) {
|
||||
this.apduChannel = apduChannel;
|
||||
this.cardKeys = new SCP02Keys(testKey, testKey);
|
||||
setCardKeys(developmentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given key as all of ENC, MAC and DEK static keys, used to derive session keys.
|
||||
* @param key the key
|
||||
*/
|
||||
public void setCardKeys(byte[] key) {
|
||||
setCardKeys(key, key, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the the ENC, MAC and DEK static keys, used to derive session keys.
|
||||
*
|
||||
* @param encKey the ENC key
|
||||
* @param macKey the MAC key
|
||||
* @param dekKey the DEK key
|
||||
*/
|
||||
public void setCardKeys(byte[] encKey, byte[] macKey, byte[] dekKey) {
|
||||
this.cardKeys = new SCP02Keys(encKey, macKey, dekKey);;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +97,12 @@ public class GlobalPlatformCommandSet {
|
||||
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);
|
||||
try {
|
||||
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
|
||||
} catch(APDUException e) {
|
||||
this.session = SecureChannel.verifyChallenge(hostChallenge, gpDefaultKeys, resp);
|
||||
this.session.markAsUsingFallbackKeys();
|
||||
}
|
||||
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
|
||||
}
|
||||
|
||||
@@ -103,18 +131,93 @@ public class GlobalPlatformCommandSet {
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens an SCP02 secure channel with default keys.
|
||||
* Convenience method for openSecureChannel with auto key ugprade.
|
||||
*
|
||||
* @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 {
|
||||
openSecureChannel(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SCP02 secure channel. If with the current keys the card cryptogram cannot be verified, an attempt is made
|
||||
* to use the default GlobalPlatform keys instead. This does not require additional commands to the card. In case
|
||||
* the autoUpgradeKeys is set to true and the default GlobalPlatform keys were used, a PUT KEY command is sent to
|
||||
* change the keys to the current ones.
|
||||
*
|
||||
* @param autoUpgradeKeys upgrade keys if default GP keys are loaded
|
||||
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void openSecureChannel(boolean autoUpgradeKeys) throws APDUException, IOException {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] hostChallenge = new byte[8];
|
||||
random.nextBytes(hostChallenge);
|
||||
initializeUpdate(hostChallenge).checkOK();
|
||||
externalAuthenticate(hostChallenge).checkOK();
|
||||
|
||||
if (this.session.usesFallbackKeys() && autoUpgradeKeys) {
|
||||
this.putSCP02Keys(this.cardKeys.getEncKeyData(), this.cardKeys.getMacKeyData(), this.cardKeys.getDekKeyData(), 0, 1).checkOK();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PUT KEY APDU to load or replace SCP02 keys. The key is used for all 3 of ENC, MAC and DEK.
|
||||
*
|
||||
* @param key the key to load
|
||||
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
|
||||
* @param newKvn the KVN of the new keyset
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public APDUResponse putSCP02Keys(byte[] key, int oldKvn, int newKvn) throws IOException {
|
||||
return putSCP02Keys(key, key, key, oldKvn, newKvn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PUT KEY APDU to load or replace SCP02 keys. The keys are assumed to be 3DES keys
|
||||
*
|
||||
* @param encKey the ENC key to load
|
||||
* @param macKey the MAC key to load
|
||||
* @param dekKey the DEK key to load
|
||||
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
|
||||
* @param newKvn the KVN of the new keyset
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public APDUResponse putSCP02Keys(byte[] encKey, byte[] macKey, byte[] dekKey, int oldKvn, int newKvn) throws IOException {
|
||||
if (encKey.length != 16 || macKey.length != 16 || dekKey.length != 16){
|
||||
throw new IllegalArgumentException("All keys must be 16-byte 3DES keys");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
bos.write(newKvn);
|
||||
writeSCP02Key(bos, encKey);
|
||||
writeSCP02Key(bos, macKey);
|
||||
writeSCP02Key(bos, dekKey);
|
||||
|
||||
APDUCommand cmd = new APDUCommand(0x84, INS_PUT_KEY, oldKvn, 0x81, bos.toByteArray());
|
||||
return this.secureChannel.send(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* writes an encrypted key for the PUT KEY command
|
||||
* @param bos the output stream to write to
|
||||
* @param key the key to encrypt and write
|
||||
* @throws IOException if the ByteArrayOutputStream throws it (never)
|
||||
*/
|
||||
private void writeSCP02Key(ByteArrayOutputStream bos, byte[] key) throws IOException {
|
||||
byte[] encrypted = Crypto.ecb3des(session.getKeys().getDekKeyData(), key);
|
||||
byte[] kcv = Crypto.kcv3des(key);
|
||||
|
||||
bos.write(0x80);
|
||||
bos.write(encrypted.length);
|
||||
bos.write(encrypted);
|
||||
bos.write(kcv.length);
|
||||
bos.write(kcv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +230,16 @@ public class GlobalPlatformCommandSet {
|
||||
return delete(Identifiers.getKeycardInstanceAID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Keycard Cash applet instance.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deleteCashInstance() throws IOException {
|
||||
return delete(Identifiers.CASH_INSTANCE_AID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the NDEF applet instance.
|
||||
*
|
||||
@@ -157,6 +270,7 @@ public class GlobalPlatformCommandSet {
|
||||
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);
|
||||
deleteCashInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -313,4 +427,25 @@ public class GlobalPlatformCommandSet {
|
||||
public APDUResponse installKeycardApplet() throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.KEYCARD_AID, Identifiers.getKeycardInstanceAID(), new byte[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Cash applet.
|
||||
*
|
||||
* @param cashData the initial Cash data. Can be a zero-length array but not null
|
||||
* @return the card response
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse installCashApplet(byte[] cashData) throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.CASH_AID, Identifiers.CASH_INSTANCE_AID, cashData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Cash applet.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse installCashApplet() throws IOException {
|
||||
return installCashApplet(new byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,19 @@ package im.status.keycard.globalplatform;
|
||||
public class SCP02Keys {
|
||||
public byte[] encKeyData;
|
||||
public byte[] macKeyData;
|
||||
public byte[] dekKeyData;
|
||||
|
||||
/**
|
||||
* Constructor. Takes the ENC and MAC keys.
|
||||
*
|
||||
* @param encKeyData encryption key
|
||||
* @param macKeyData mac key
|
||||
* @param dekKeyData data encryption key
|
||||
*/
|
||||
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
|
||||
public SCP02Keys(byte[] encKeyData, byte[] macKeyData, byte[] dekKeyData) {
|
||||
this.encKeyData = encKeyData;
|
||||
this.macKeyData = macKeyData;
|
||||
this.dekKeyData = dekKeyData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,4 +37,13 @@ public class SCP02Keys {
|
||||
public byte[] getMacKeyData() {
|
||||
return macKeyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The DEK key
|
||||
*
|
||||
* @return the DEK key
|
||||
*/
|
||||
public byte[] getDekKeyData() {
|
||||
return dekKeyData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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.
|
||||
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the secure channel.
|
||||
*/
|
||||
public class SecureChannel {
|
||||
private CardChannel channel;
|
||||
@@ -77,8 +77,9 @@ public class SecureChannel {
|
||||
|
||||
byte[] sessionEncKey = Crypto.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
|
||||
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
|
||||
byte[] sessionDekKey = Crypto.deriveSCP02SessionKey(cardKeys.getDekKeyData(), seq, DERIVATION_PURPOSE_DEK);
|
||||
|
||||
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
|
||||
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey, sessionDekKey);
|
||||
|
||||
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
|
||||
if (!verified) {
|
||||
|
||||
@@ -6,6 +6,7 @@ package im.status.keycard.globalplatform;
|
||||
public class Session {
|
||||
private SCP02Keys keys;
|
||||
private byte[] cardChallenge;
|
||||
private boolean fallbackKeys;
|
||||
|
||||
/**
|
||||
* Constructs the SCP02 session.
|
||||
@@ -16,6 +17,7 @@ public class Session {
|
||||
public Session(SCP02Keys keys, byte[] cardChallenge) {
|
||||
this.keys = keys;
|
||||
this.cardChallenge = cardChallenge;
|
||||
this.fallbackKeys = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,4 +35,21 @@ public class Session {
|
||||
public byte[] getCardChallenge() {
|
||||
return cardChallenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this session as using a fallback keyset.
|
||||
*/
|
||||
public void markAsUsingFallbackKeys() {
|
||||
fallbackKeys = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if a fallback keyset is being used.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean usesFallbackKeys() {
|
||||
return fallbackKeys;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,14 @@ 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
|
||||
*/
|
||||
int pairingPasswordPBKDF2IterationCount();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package im.status.keycard.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class LedgerUtil {
|
||||
private static final int LEDGER_DEFAULT_CHANNEL = 1;
|
||||
private static final int TAG_APDU = 0x05;
|
||||
|
||||
private LedgerUtil() {}
|
||||
|
||||
public interface Callback {
|
||||
void write(byte[] chunk) throws IOException;
|
||||
void read(byte[] chunk) throws IOException;
|
||||
}
|
||||
|
||||
public static APDUResponse send(APDUCommand cmd, int segmentSize, boolean channelInfo, LedgerUtil.Callback cb) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
byte[] command = LedgerUtil.wrapCommandAPDU(cmd.serialize(), segmentSize, channelInfo);
|
||||
byte[] chunk = new byte[segmentSize];
|
||||
|
||||
while(offset != command.length) {
|
||||
System.arraycopy(command, offset, chunk, 0, segmentSize);
|
||||
cb.write(chunk);
|
||||
offset += segmentSize;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
byte[] responseData = null;
|
||||
|
||||
while ((responseData = LedgerUtil.unwrapResponseAPDU(response.toByteArray(), segmentSize, channelInfo)) == null) {
|
||||
cb.read(chunk);
|
||||
response.write(chunk, 0, segmentSize);
|
||||
}
|
||||
|
||||
return new APDUResponse(responseData);
|
||||
}
|
||||
|
||||
private static byte[] unwrapResponseAPDU(byte[] data, int segmentSize, boolean channelInfo) throws IOException {
|
||||
if ((data == null) || (data.length < 7 + 5)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int sequenceIdx = 0;
|
||||
int offset = checkResponseHeader(data, 0, sequenceIdx, channelInfo);
|
||||
|
||||
int responseLength = ((data[offset++] & 0xff) << 8);
|
||||
responseLength |= (data[offset++] & 0xff);
|
||||
|
||||
if (data.length < 7 + responseLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
|
||||
int headerSize = channelInfo ? 5 : 3;
|
||||
int initialHeaderSize = headerSize + 2;
|
||||
|
||||
int blockSize = (responseLength > segmentSize - initialHeaderSize ? segmentSize - initialHeaderSize : responseLength);
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
|
||||
while (response.size() != responseLength) {
|
||||
sequenceIdx++;
|
||||
|
||||
if (offset == data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
offset = checkResponseHeader(data, offset, sequenceIdx, channelInfo);
|
||||
|
||||
blockSize = (responseLength - response.size() > segmentSize - headerSize ? segmentSize - headerSize : responseLength - response.size());
|
||||
if (blockSize > data.length - offset) {
|
||||
return null;
|
||||
}
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
|
||||
return response.toByteArray();
|
||||
}
|
||||
|
||||
private static int checkResponseHeader(byte[] data, int offset, int sequenceIdx, boolean channelInfo) throws IOException {
|
||||
if (channelInfo) {
|
||||
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 static byte[] wrapCommandAPDU(byte[] command, int segmentSize, boolean channelInfo) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
int headerSize = channelInfo ? 5 : 3;
|
||||
int initialHeaderSize = headerSize + 2;
|
||||
|
||||
int sequenceIdx = 0;
|
||||
int offset = 0;
|
||||
writeCommandHeader(output, sequenceIdx, channelInfo);
|
||||
sequenceIdx++;
|
||||
|
||||
output.write(command.length >> 8);
|
||||
output.write(command.length);
|
||||
int blockSize = (command.length > (segmentSize - initialHeaderSize) ? (segmentSize - initialHeaderSize) : command.length);
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
|
||||
while (offset != command.length) {
|
||||
writeCommandHeader(output, sequenceIdx, channelInfo);
|
||||
sequenceIdx++;
|
||||
|
||||
blockSize = ((command.length - offset) > (segmentSize - headerSize) ? (segmentSize - headerSize) : (command.length - offset));
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
|
||||
if ((output.size() % segmentSize) != 0) {
|
||||
byte[] padding = new byte[segmentSize - (output.size() % segmentSize)];
|
||||
output.write(padding, 0, padding.length);
|
||||
}
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx, boolean channelInfo) {
|
||||
if (channelInfo) {
|
||||
output.write(LEDGER_DEFAULT_CHANNEL >> 8);
|
||||
output.write(LEDGER_DEFAULT_CHANNEL);
|
||||
}
|
||||
|
||||
output.write(TAG_APDU);
|
||||
output.write(sequenceIdx >> 8);
|
||||
output.write(sequenceIdx);
|
||||
}
|
||||
}
|
||||
@@ -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