Compare commits

..
Author SHA1 Message Date
Andrea Franz 5a20f8f601 remove default and implementation from interface 2019-11-20 15:38:04 +01:00
ligi 3f8966f1a8 Make setNDEF backward compatible to the 2.x style (#19) 2019-10-23 14:21:12 +03:00
Michele Balistreri 3acea10750 hardcode english dictionary 2019-10-23 09:59:44 +03:00
Michele Balistreri 8cb43e6717 fix typo 2019-10-16 14:46:06 +03:00
Michele Balistreri 6bf8da8374 add generic STORE DATA/GET DATA method, reimplement setNDEF 2019-10-16 14:11:41 +03:00
Michele Balistreri 536bad2671 add data to cash applet 2019-10-16 13:30:21 +03:00
Michele Balistreri b1be261ea1 remove DUPLICATE KEY command 2019-10-15 14:55:48 +03:00
Bitgamma 4a69788473 V2.3 (#18)
* add STORE/GET DATA commands

* add basic CashCommandSet and installation methods

* remove P1

* add CashApplicationInfo

* add Nano X support

* (unfinished) BLE support

* refactor ledger protocol support for better code reuse

* remove spurious declaration

* finish ble implementation

* enable notifications

* fixed segmentation over BLE

* update GlobalPlatform's delete method
2019-09-02 13:40:06 +03:00
Bitgamma eed0b09fed Create LICENSE 2019-04-29 09:26:17 +03:00
26 changed files with 3005 additions and 406 deletions
+201
View File
@@ -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.
+2 -2
View File
@@ -8,8 +8,8 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 201
versionName "2.0.1"
versionCode 300
versionName "3.0.0"
}
compileOptions {
+6 -1
View File
@@ -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;
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ android {
applicationId "im.status.keycard.demo"
minSdkVersion 19
targetSdkVersion 28
versionCode 201
versionName "2.0.1"
versionCode 300
versionName "3.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -1,9 +1,12 @@
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;
@@ -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) {
@@ -159,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,158 +3,38 @@ 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.ByteArrayOutputStream;
import java.io.IOException;
public class LedgerUSBChannel implements CardChannel {
private static final int HID_BUFFER_SIZE = 64;
private static final int LEDGER_DEFAULT_CHANNEL = 1;
private static final int TAG_APDU = 0x05;
private static final int READ_TIMEOUT = 20000;
private HidDevice hidDevice;
public LedgerUSBChannel(HidDevice hidDevice) {
this.hidDevice = hidDevice;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
ByteArrayOutputStream response = new ByteArrayOutputStream();
int offset = 0;
byte[] command = wrapCommandAPDU(cmd.serialize());
byte[] chunk = new byte[HID_BUFFER_SIZE];
while(offset != command.length) {
System.arraycopy(command, offset, chunk, 0, HID_BUFFER_SIZE);
if (hidDevice.write(chunk, HID_BUFFER_SIZE, (byte) 0x00) < 0) {
throw new IOException("Write failed");
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");
}
}
offset += HID_BUFFER_SIZE;
}
byte[] responseData = null;
while ((responseData = unwrapResponseAPDU(response.toByteArray())) == null) {
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
throw new IOException("Read failed");
@Override
public void read(byte[] chunk) throws IOException {
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
throw new IOException("Read failed");
}
}
response.write(chunk, 0, HID_BUFFER_SIZE);
}
return new APDUResponse(responseData);
}
private byte[] unwrapResponseAPDU(byte[] data) throws IOException {
if ((data == null) || (data.length < 7 + 5)) {
return null;
}
int sequenceIdx = 0;
int offset = checkResponseHeader(data, 0, sequenceIdx);
int responseLength = ((data[offset++] & 0xff) << 8);
responseLength |= (data[offset++] & 0xff);
if (data.length < 7 + responseLength) {
return null;
}
ByteArrayOutputStream response = new ByteArrayOutputStream();
int blockSize = (responseLength > HID_BUFFER_SIZE - 7 ? HID_BUFFER_SIZE - 7 : responseLength);
response.write(data, offset, blockSize);
offset += blockSize;
while (response.size() != responseLength) {
sequenceIdx++;
if (offset == data.length) {
return null;
}
offset = checkResponseHeader(data, offset, sequenceIdx);
blockSize = (responseLength - response.size() > HID_BUFFER_SIZE - 5 ? HID_BUFFER_SIZE - 5 : responseLength - response.size());
if (blockSize > data.length - offset) {
return null;
}
response.write(data, offset, blockSize);
offset += blockSize;
}
return response.toByteArray();
}
private int checkResponseHeader(byte[] data, int offset, int sequenceIdx) throws IOException {
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL >> 8)) {
throw new IOException("Invalid channel");
}
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL & 0xff)) {
throw new IOException("Invalid channel");
}
if (data[offset++] != TAG_APDU) {
throw new IOException("Invalid tag");
}
if (data[offset++] != (sequenceIdx >> 8)) {
throw new IOException("Invalid sequence");
}
if (data[offset++] != (sequenceIdx & 0xff)) {
throw new IOException("Invalid sequence");
}
return offset;
}
private byte[] wrapCommandAPDU(byte[] command) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
int sequenceIdx = 0;
int offset = 0;
writeCommandHeader(output, sequenceIdx);
sequenceIdx++;
output.write(command.length >> 8);
output.write(command.length);
int blockSize = (command.length > (HID_BUFFER_SIZE - 7) ? (HID_BUFFER_SIZE - 7) : command.length);
output.write(command, offset, blockSize);
offset += blockSize;
while (offset != command.length) {
writeCommandHeader(output, sequenceIdx);
sequenceIdx++;
blockSize = ((command.length - offset) > (HID_BUFFER_SIZE - 5) ? (HID_BUFFER_SIZE - 5) : (command.length - offset));
output.write(command, offset, blockSize);
offset += blockSize;
}
if ((output.size() % HID_BUFFER_SIZE) != 0) {
byte[] padding = new byte[HID_BUFFER_SIZE - (output.size() % HID_BUFFER_SIZE)];
output.write(padding, 0, padding.length);
}
return output.toByteArray();
}
private void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx) {
output.write(LEDGER_DEFAULT_CHANNEL >> 8);
output.write(LEDGER_DEFAULT_CHANNEL);
output.write(TAG_APDU);
output.write(sequenceIdx >> 8);
output.write(sequenceIdx);
});
}
@Override
@@ -11,7 +11,8 @@ public class LedgerUSBManager implements HidServicesListener {
}
private static final int VID = 0x2c97;
private static final int PID = 0x0001;
private static final int[] PIDS = { 0x0001, 0x0004 };
private static final int SCAN_INTERVAL_MS = 500;
private static final int PAUSE_INTERVAL_MS = 5000;
@@ -34,10 +35,13 @@ public class LedgerUSBManager implements HidServicesListener {
public void start() {
hidServices.start();
HidDevice hidDevice = hidServices.getHidDevice(VID, PID, null);
for (int pid : PIDS) {
HidDevice hidDevice = hidServices.getHidDevice(VID, pid, null);
if (hidDevice != null) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
if (hidDevice != null) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
break;
}
}
}
@@ -49,7 +53,7 @@ public class LedgerUSBManager implements HidServicesListener {
public void hidDeviceAttached(HidServicesEvent event) {
HidDevice hidDevice = event.getHidDevice();
if (hidDevice.isVidPidSerial(VID, PID, null)) {
if (isLedger(hidDevice)) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
}
@@ -62,10 +66,18 @@ public class LedgerUSBManager implements HidServicesListener {
@Override
public void hidFailure(HidServicesEvent event) {
HidDevice hidDevice = event.getHidDevice();
if (hidDevice.isVidPidSerial(VID, PID, null)) {
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;
}
}
@@ -47,4 +47,8 @@ public class PCSCCardChannel implements CardChannel {
public boolean isConnected() {
return true;
}
public int pairingPasswordPBKDF2IterationCount() {
return 50000;
}
}
@@ -118,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);
}
@@ -148,18 +156,38 @@ public class ApplicationInfo {
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,141 +0,0 @@
package im.status.keycard.applet;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.WrongPINException;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashSet;
/**
* Class helping with the card duplication process. Depending on the client's role, only some of the methods are relevant.
*/
public class CardDuplicator {
private byte[] secret;
private KeycardCommandSet cmdSet;
private DuplicatorCallback cb;
private HashSet<byte[]> startedDuplication;
private HashSet<byte[]> addedEntropy;
private HashSet<byte[]> finishedDuplication;
/**
* Creates a CardDuplicator object. Regardless of the role of the client, this object must be kept and used for the
* entire duplication session. It cannot be reused for multiple sessions.
*
* @param cmdSet the CommandSet to use
* @param cb the callback object for backups. This is needed only on the client performing steps requiring pairing
* and authentication. Clients which only add entropy should pass null
*/
public CardDuplicator(KeycardCommandSet cmdSet, DuplicatorCallback cb) {
this.cmdSet = cmdSet;
this.cb = cb;
this.startedDuplication = new HashSet<>();
this.addedEntropy = new HashSet<>();
this.finishedDuplication = new HashSet<>();
this.secret = new byte[32];
SecureRandom random = new SecureRandom();
random.nextBytes(this.secret);
}
/**
* Creates a CardDuplicator object. Only suitable for clients performing the role of adding entropy.
*
* @param channel the APDU channel
*/
public CardDuplicator(CardChannel channel) {
this(new KeycardCommandSet(channel), null);
}
private ApplicationInfo selectAndCheck(HashSet<byte[]> processed) throws APDUException, IOException {
ApplicationInfo appInfo = new ApplicationInfo(cmdSet.select().checkOK().getData());
if (!processed.add(appInfo.getInstanceUID())) {
throw new IllegalStateException("The requested action has been already performed on this card");
}
return appInfo;
}
private void preamble(HashSet<byte[]> processed) throws IOException, APDUException {
ApplicationInfo appInfo = selectAndCheck(processed);
Pairing pairing = cb.getPairing(appInfo);
if (pairing == null) {
throw new APDUException("The given card is not paired");
}
cmdSet.setPairing(pairing);
cmdSet.autoOpenSecureChannel();
ApplicationStatus appStatus = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
int remainingAttempts = appStatus.getPINRetryCount();
while(remainingAttempts > 0) {
try {
cmdSet.verifyPIN(cb.getPIN(appInfo, remainingAttempts)).checkAuthOK();
break;
} catch(WrongPINException e) {
remainingAttempts = e.getRetryAttempts();
}
}
if (remainingAttempts <= 0) {
throw new APDUException("Card blocked");
}
}
/**
* Starts duplication session. Must be used on all cards taking part of in the duplication process.
*
* @param clientCount the number of clients which will be adding entropy for the key, including this one
*
* @throws IOException communication error
* @throws APDUException unexpected card response
* @throws IllegalStateException this card has already been used
*/
public void startDuplication(int clientCount) throws IOException, APDUException, IllegalStateException {
preamble(startedDuplication);
cmdSet.duplicateKeyStart(clientCount, secret).checkOK();
}
/**
* Exports key. Must be used on the card designated as the source for the duplication.
*
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] exportKey() throws IOException, APDUException, IllegalStateException {
preamble(finishedDuplication);
return cmdSet.duplicateKeyExport().checkOK().getData();
}
/**
* Imports key. Must be used on all cards designated as the target for the duplication.
*
* @param key the key to import
* @return the key UID
* @throws IOException communication error
* @throws APDUException unexpected card response
* @throws IllegalStateException this card has already been used
*/
public byte[] importKey(byte[] key) throws IOException, APDUException, IllegalStateException {
preamble(finishedDuplication);
return cmdSet.duplicateKeyImport(key).checkOK().getData();
}
/**
* Adds entropy. Must be used on all cards taking part in the backup process. Each client taking part must use this
* exactly once, except for the client which started the backup.
*
* @throws IOException communication error
* @throws APDUException unexpected card response
* @throws IllegalStateException this card has already been used
*/
public void addEntropy() throws IOException, APDUException, IllegalStateException {
selectAndCheck(addedEntropy);
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
}
}
@@ -0,0 +1,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);
}
}
@@ -1,24 +0,0 @@
package im.status.keycard.applet;
/**
* Callback interface for duplication procedure.
*/
public interface DuplicatorCallback {
/**
* Must return the pairing for the current card, represented by the applicationInfo parameter. If no pairing
* could be found, null must be returned.
*
* @param applicationInfo the application info template of the currently inserted card
* @return the pairing info or null
*/
Pairing getPairing(ApplicationInfo applicationInfo);
/**
* Must return the PIN for the current card. This method can prompt the user or return a cached value.
*
* @param applicationInfo the application info template of the currently inserted card
* @param remainingAttempts the number of remaining PIN attempts
* @return the PIN
*/
String getPIN(ApplicationInfo applicationInfo, int remainingAttempts);
}
@@ -0,0 +1,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);
}
}
@@ -13,6 +13,9 @@ public class Identifiers {
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 default instance of the Keycard applet.
*
@@ -28,10 +28,11 @@ 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;
@@ -58,6 +59,10 @@ public class KeycardCommandSet {
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;
@@ -267,19 +272,7 @@ 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.
*
@@ -486,57 +479,6 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, generateKey);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 00, P2 to the entropy count and the data is the first entropy piece.
* This starts a duplication session. Requires an open Secure Channel and authenticated PIN.
*
* @param entropyCount the number of entropy pieces to expect, including the one in this APDU
* @param firstEntropy a random 32-byte number
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyStart(int entropyCount, byte[] firstEntropy) throws IOException {
APDUCommand duplicateKeyStart = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_START, entropyCount, firstEntropy);
return secureChannel.transmit(apduChannel, duplicateKeyStart);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 01 and the data is the entropy. This adds entropy and does not require
* a Secure Channel or authenticated PIN.
*
* @param entropy a random 32-byte number
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyAddEntropy(byte[] entropy) throws IOException {
APDUCommand duplicateKeyAddEntropy = new APDUCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_ADD_ENTROPY, 0, secureChannel.oneShotEncrypt(entropy));
return apduChannel.send(duplicateKeyAddEntropy);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 02. This exports the encrypted master key including chaining code.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyExport() throws IOException {
APDUCommand duplicateKeyExport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_EXPORT, 0, new byte[0]);
return secureChannel.transmit(apduChannel, duplicateKeyExport);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 03. This imports an encrypted master key including chaining code. The
* response data contains the key UID of the imported key.
*
* @param key the key, exported from another card in the same duplication session.
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyImport(byte[] key) throws IOException {
APDUCommand duplicateKeyImport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_IMPORT, 0, key);
return secureChannel.transmit(apduChannel, duplicateKeyImport);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
*
@@ -578,7 +520,6 @@ public class KeycardCommandSet {
return sign(hash, SIGN_P1_PINLESS);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
* a derivation path.
@@ -723,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.
*
@@ -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
@@ -105,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);
@@ -230,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.
*
@@ -260,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);
}
@@ -416,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]);
}
}
@@ -29,7 +29,5 @@ public interface CardChannel {
*
* @return the iteration count
*/
default int pairingPasswordPBKDF2IterationCount() {
return 50000;
}
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);
}
}