Compare commits

...
Author SHA1 Message Date
Audrius Molis d3bd7cd2ff test adding exception info to the RuntimeException in RuntimeException 2022-09-09 16:20:02 +03:00
Michele Balistreri bbcce01742 add metadata parser/encoder (#22) 2022-07-18 14:16:36 +02:00
Michele Balistreri a39924aba3 BLS support (#21)
add support for BLS
2022-07-18 14:05:43 +02:00
Michele Balistreri 22db82e8f2 add init with pin/puk retries 2021-12-23 09:29:21 +03:00
Michele Balistreri 9295aa6553 check OPEN SECURE CHANNEL response 2020-11-09 15:36:50 +01:00
Michele Balistreri 86e6cb60ec bc 1.60 2020-11-09 14:03:25 +01:00
Michele Balistreri aaf2c9d9e6 update README 2020-06-02 10:22:27 +03:00
Michele Balistreri 9431d7c497 differentiate between communication errors and unexpected APDU response in the "auto" methods of the SecureChannelSession 2020-06-02 09:44:11 +03:00
Michele Balistreri f97363704b Merge branch 'master' of github.com:status-im/status-keycard-java 2020-04-15 13:52:10 +03:00
Michele Balistreri 144474415d closes #20 2020-04-15 13:46:49 +03: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
Michele Balistreri 7ce0136b1d auto upgrade to new keys 2019-04-05 16:09:57 +03:00
Michele Balistreri 2865d2a08a use key identifier 1 2019-04-05 13:26:58 +03:00
Michele Balistreri 3e5bb577d7 indicate multiple keys in P2 2019-04-05 13:17:11 +03:00
Michele Balistreri e882e39105 use correct encryption scheme 2019-04-05 12:44:05 +03:00
Michele Balistreri 15a2c43a70 actually save the DEK key 2019-04-05 12:38:05 +03:00
Michele Balistreri 205f150705 add PUT KEY command 2019-04-05 12:22:00 +03:00
Bitgamma a4ff736d6e Keycard v2.2 (#15)
* add methods for the extended SIGN command

* add resetPinlessPath
2019-04-04 10:20:38 +03:00
35 changed files with 4375 additions and 463 deletions
+3
View File
@@ -1,8 +1,11 @@
*.iml
.gradle
.vscode
/local.properties
.idea
.DS_Store
/build
/captures
/desktop/bin
lib/bin
.externalNativeBuild
+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
@@ -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.1.1'
implementation 'com.github.status-im.status-keycard-java:android:3.0.2'
}
```
@@ -23,6 +23,6 @@ dependencies {
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:2.1.1'
implementation 'com.github.status-im.status-keycard-java:desktop:3.0.2'
}
```
+2 -2
View File
@@ -8,8 +8,8 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 201
versionName "2.0.1"
versionCode 304
versionName "3.0.4"
}
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;
}
}
+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
+14
View File
@@ -1,5 +1,19 @@
import org.gradle.plugins.ide.eclipse.model.AccessRule
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'eclipse'
eclipse {
classpath {
file {
whenMerged {
def jre = entries.find { it.path.contains 'org.eclipse.jdt.launching.JRE_CONTAINER' }
jre.accessRules.add(new AccessRule('0', 'javax/smartcardio/**'))
}
}
}
}
dependencies {
compile project(':lib')
@@ -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;
}
}
@@ -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;
}
@@ -1,6 +1,5 @@
package im.status.keycard.applet;
import org.bouncycastle.crypto.digests.KeccakDigest;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Mac;
@@ -171,12 +170,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);
}
/**
@@ -0,0 +1,863 @@
package im.status.keycard.applet;
import java.math.BigInteger;
import java.security.DigestException;
import java.security.MessageDigest;
import java.util.Arrays;
public class BLS {
public static byte[] hash(byte[] msg) {
Fp[][] u = hashToField(msg, 2);
PointG2 q0 = isogenyMapG2(mapToCurveSimpleSWU9mod16(new Fp2(u[0][0], u[0][1])));
PointG2 q1 = isogenyMapG2(mapToCurveSimpleSWU9mod16(new Fp2(u[1][0], u[1][1])));
PointG2 r = q0.add(q1).clearCofactor();
return r.toByteArray(false);
}
public static byte[] compress(byte[] g2) {
return new PointG2(g2).toByteArray(true);
}
private BLS() {}
final static byte DST[] = {
(byte) 0x42, (byte) 0x4C, (byte) 0x53, (byte) 0x5F, (byte) 0x53, (byte) 0x49, (byte) 0x47, (byte) 0x5F,
(byte) 0x42, (byte) 0x4C, (byte) 0x53, (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x38, (byte) 0x31,
(byte) 0x47, (byte) 0x32, (byte) 0x5F, (byte) 0x58, (byte) 0x4D, (byte) 0x44, (byte) 0x3A, (byte) 0x53,
(byte) 0x48, (byte) 0x41, (byte) 0x2D, (byte) 0x32, (byte) 0x35, (byte) 0x36, (byte) 0x5F, (byte) 0x53,
(byte) 0x53, (byte) 0x57, (byte) 0x55, (byte) 0x5F, (byte) 0x52, (byte) 0x4F, (byte) 0x5F, (byte) 0x4E,
(byte) 0x55, (byte) 0x4C, (byte) 0x5F, (byte) 0x2B,
};
final private static int L = 64;
final private static int M = 2;
final private static int SHA256_DIGEST_SIZE = 32;
final private static BigInteger P = new BigInteger("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16);
final private static BigInteger P_MINUS_9_DIV_16 = P.pow(2).subtract(BigInteger.valueOf(9)).divide(BigInteger.valueOf(16));
final private static BigInteger CURVE_X = new BigInteger("d201000000010000", 16);
final private static Fp rv1 = new Fp("6af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09");
final private static Fp ev1 = new Fp("699be3b8c6870965e5bf892ad5d2cc7b0e85a117402dfd83b7f4a947e02d978498255a2aaec0ac627b5afbdf1bf1c90");
final private static Fp ev2 = new Fp("8157cd83046453f5dd0972b6e3949e4288020b5b8a9cc99ca07e27089a2ce2436d965026adad3ef7baba37f2183e9b5");
final private static Fp ev3 = new Fp("ab1c2ffdd6c253ca155231eb3e71ba044fd562f6f72bc5bad5ec46a0b7a3b0247cf08ce6c6317f40edbc653a72dee17");
final private static Fp ev4 = new Fp("aa404866706722864480885d68ad0ccac1967c7544b447873cc37e0181271e006df72162a3d3e0287bf597fbf7f8fc1");
final private static Fp PSI2_C1 = new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac");
final private static Fp2[] xnum = new Fp2[] {
new Fp2(new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6"),
new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6")),
new Fp2(Fp.ZERO,
new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71a")),
new Fp2(new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71e"),
new Fp("8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38d")),
new Fp2(new Fp("171d6541fa38ccfaed6dea691f5fb614cb14b4e7f4e810aa22d6108f142b85757098e38d0f671c7188e2aaaaaaaa5ed1"),
Fp.ZERO),
};
final private static Fp2[] xden = new Fp2[] {
new Fp2(Fp.ZERO,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa63")),
new Fp2(new Fp(0xc),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa9f")),
Fp2.ONE,
Fp2.ZERO,
};
final private static Fp2[] ynum = new Fp2[] {
new Fp2(new Fp("1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706"),
new Fp("1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706")),
new Fp2(Fp.ZERO,
new Fp("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97be")),
new Fp2(new Fp("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71c"),
new Fp("8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38f")),
new Fp2(new Fp("124c9ad43b6cf79bfbf7043de3811ad0761b0f37a1e26286b0e977c69aa274524e79097a56dc4bd9e1b371c71c718b10"),
Fp.ZERO),
};
final private static Fp2[] yden = new Fp2[] {
new Fp2(new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb"),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb")),
new Fp2(Fp.ZERO,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa9d3")),
new Fp2(new Fp(0x12),
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa99")),
new Fp2(Fp.ONE, Fp.ZERO),
};
final private static Fp2[][] ISOGENY_COEFFICIENTS = new Fp2[][] { xnum, xden, ynum, yden };
final private static Fp2[] FP2_ROOTS_OF_UNITY = new Fp2[] {
Fp2.ONE,
new Fp2(rv1, rv1.neg()),
new Fp2(Fp.ZERO, Fp.ONE),
new Fp2(rv1, rv1),
new Fp2(Fp.ONE.neg(), Fp.ZERO),
new Fp2(rv1.neg(), rv1),
new Fp2(Fp.ZERO, Fp.ONE.neg()),
new Fp2(rv1.neg(), rv1.neg()),
};
final private static Fp2[] FP2_ETAs = new Fp2[] {
new Fp2(ev1, ev2),
new Fp2(ev2.neg(), ev1),
new Fp2(ev3, ev4),
new Fp2(ev4.neg(), ev3),
};
private static byte[] strxor(byte[] b0, byte[] b1, int b1off) {
byte[] xored = new byte[b0.length];
for (int i = 0; i < xored.length; i++) {
xored[i] = (byte) (b0[i] ^ b1[i + b1off]);
}
return xored;
}
private static byte[] expandMessage(byte[] msg, byte[] DST, int len) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
} catch (Exception e) {
throw new RuntimeException("SHA256 missing");
}
int ell = (len + (SHA256_DIGEST_SIZE - 1)) / SHA256_DIGEST_SIZE;
md.update(new byte[SHA256_DIGEST_SIZE * 2]);
md.update(msg);
md.update(new byte[] { (byte) ((len >> 8) & 0xff), (byte) (len & 0xff), (byte) 0 });
md.update(DST);
byte[] b0 = md.digest();
byte[] b = new byte[ell * SHA256_DIGEST_SIZE];
for (int i = 0; i < ell; i++) {
if (i == 0) {
md.update(b0);
} else {
md.update(strxor(b0, b, ((i - 1) * SHA256_DIGEST_SIZE)));
}
md.update((byte) (i + 1));
md.update(DST);
try {
md.digest(b, (i * SHA256_DIGEST_SIZE), SHA256_DIGEST_SIZE);
} catch (DigestException e) {
throw new RuntimeException("SHA256 error");
}
}
return Arrays.copyOf(b, len);
}
private static Fp[][] hashToField(byte[] msg, int count) {
byte[] uniformBytes = expandMessage(msg, DST, count * M * L);
Fp[][] u = new Fp[count][M];
for (int i = 0; i < count; i++) {
for (int j = 0; j < M; j++) {
int off = (L * (j + (i * M)));
u[i][j] = new Fp(Arrays.copyOfRange(uniformBytes, off, off + L));
}
}
return u;
}
private static PointG2 isogenyMapG2(PointG2 point) {
Fp2[] zPowers = new Fp2[] {point.z, point.z.square(), point.z.pow(3)};
Fp2[] mapped = new Fp2[] {Fp2.ZERO, Fp2.ZERO, Fp2.ZERO, Fp2.ZERO};
for (int i = 0; i < ISOGENY_COEFFICIENTS.length; i++) {
Fp2[] kI = ISOGENY_COEFFICIENTS[i];
mapped[i] = kI[3];
Fp2[] arr = new Fp2[] { kI[2], kI[1], kI[0] };
for (int j = 0; j < arr.length; j++) {
Fp2 kIJ = arr[j];
mapped[i] = mapped[i].mul(point.x).add(zPowers[j].mul(kIJ));
}
}
mapped[2] = mapped[2].mul(point.y);
mapped[3] = mapped[3].mul(point.z);
Fp2 z2 = mapped[1].mul(mapped[3]);
Fp2 x2 = mapped[0].mul(mapped[3]);
Fp2 y2 = mapped[1].mul(mapped[2]);
return new PointG2(x2, y2, z2);
}
private static SqrtDivFp2Res sqrtDivFp2(Fp2 u, Fp2 v) {
Fp2 v7 = v.pow(7);
Fp2 uv7 = u.mul(v7);
Fp2 uv15 = uv7.mul(v7.mul(v));
Fp2 gamma = uv15.pow(P_MINUS_9_DIV_16).mul(uv7);
for (int i = 0; i < 4; i++) {
Fp2 candidate = FP2_ROOTS_OF_UNITY[i].mul(gamma);
if (candidate.square().mul(v).sub(u).isZero()) {
return new SqrtDivFp2Res(true, candidate);
}
}
return new SqrtDivFp2Res(false, gamma);
}
private static PointG2 mapToCurveSimpleSWU9mod16(Fp2 t) {
Fp2 iso3a = new Fp2(new Fp(0), new Fp(240));
Fp2 iso3b = new Fp2(new Fp(1012), new Fp(1012));
Fp2 iso3z = new Fp2(new Fp(-2), new Fp(-1));
Fp2 t2 = t.square();
Fp2 iso3zt2 = iso3z.mul(t2);
Fp2 ztzt = iso3zt2.add(iso3zt2.square());
Fp2 denominator = iso3a.mul(ztzt).neg();
Fp2 numerator = iso3b.mul(ztzt.add(Fp2.ONE));
if (denominator.isZero()) {
denominator = iso3z.mul(iso3a);
}
Fp2 v = denominator.pow(3);
Fp2 u = numerator.pow(3)
.add(iso3a.mul(numerator).mul(denominator.square()))
.add(iso3b.mul(v));
SqrtDivFp2Res sqrtCandidateOrGamma = sqrtDivFp2(u, v);
Fp2 y = null;
if (!sqrtCandidateOrGamma.success) {
u = iso3zt2.pow(3).mul(u);
Fp2 sqrtCandidateX1 = sqrtCandidateOrGamma.value.mul(t.pow(3));
for (int i = 0; i < FP2_ETAs.length; i++) {
Fp2 etaSqrtCanditate = FP2_ETAs[i].mul(sqrtCandidateX1);
if (etaSqrtCanditate.square().mul(v).sub(u).isZero()) {
y = etaSqrtCanditate;
numerator = numerator.mul(iso3zt2);
break;
}
}
} else {
y = sqrtCandidateOrGamma.value;
}
if (y == null) {
throw new RuntimeException("Hash to Curve - Optimized SWU failed");
}
if (t.sgn0() != y.sgn0()) {
y = y.neg();
}
y = y.mul(denominator);
return new PointG2(numerator, y, denominator);
}
static class Fp {
final static Fp ZERO = new Fp(BigInteger.ZERO);
final static Fp ONE = new Fp(BigInteger.ONE);
final static int SIZE = 48;
private BigInteger i;
Fp(byte[] b) {
this(new BigInteger(1, b));
}
Fp(long i) {
this(BigInteger.valueOf(i));
}
Fp(BigInteger i) {
this.i = i.mod(P);
}
Fp(String hex) {
this(new BigInteger(hex, 16));
}
Fp mul(Fp b) {
return new Fp(this.i.multiply(b.i));
}
Fp add(Fp b) {
return new Fp(this.i.add(b.i));
}
Fp sub(Fp b) {
return new Fp(this.i.subtract(b.i));
}
Fp neg() {
return new Fp(this.i.negate());
}
Fp square() {
return new Fp(this.i.pow(2));
}
Fp inv() {
return new Fp(i.modInverse(P));
}
boolean isZero() {
return this.i.signum() == 0;
}
void serialize(byte[] out, int off) {
byte[] encoded = i.toByteArray();
int padding = SIZE - encoded.length;
System.arraycopy(encoded, 0, out, off + padding, encoded.length);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Fp)) {
return false;
}
Fp b = (Fp) o;
return b.i.equals(this.i);
}
}
static class Fp2 {
final static Fp[] FROBENIUS_COEFFICIENTS = new Fp[] {
Fp.ONE,
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa")
};
final static Fp2 ZERO = new Fp2(Fp.ZERO, Fp.ZERO);
final static Fp2 ONE = new Fp2(Fp.ONE, Fp.ZERO);
final static int SIZE = Fp.SIZE * 2;
private Fp re;
private Fp im;
Fp2(Fp re, Fp im) {
this.re = re;
this.im = im;
}
Fp2(byte[] buf, int off) {
this(new Fp(Arrays.copyOfRange(buf, off + Fp.SIZE, off + Fp2.SIZE)), new Fp(Arrays.copyOfRange(buf, off, off + Fp.SIZE)));
}
int sgn0() {
boolean sign0 = this.re.i.testBit(0);
return sign0 || (this.re.isZero() && this.im.i.testBit(0)) ? 1 : 0;
}
Fp2 square() {
Fp a = this.re.add(this.im);
Fp b = this.re.sub(this.im);
Fp c = this.re.add(this.re);
return new Fp2(a.mul(b), c.mul(this.im));
}
Fp2 pow(long n) {
return this.pow(BigInteger.valueOf(n));
}
Fp2 pow(BigInteger n) {
if (n.signum() == 0) return Fp2.ONE;
if (n.equals(BigInteger.ONE)) return this;
Fp2 p = Fp2.ONE;
Fp2 d = this;
int bitLength = n.bitLength();
for (int i = 0; i < bitLength; i++) {
if (n.testBit(i)) {
p = p.mul(d);
}
d = d.square();
}
return p;
}
boolean isZero() {
return this.re.isZero() && this.im.isZero();
}
Fp2 mul(Fp2 b) {
Fp t1 = this.re.mul(b.re);
Fp t2 = this.im.mul(b.im);
return new Fp2(t1.sub(t2), this.re.add(this.im).mul(b.re.add(b.im)).sub(t1.add(t2)));
}
Fp2 mul(long b) {
return mul(new Fp(b));
}
Fp2 mul(Fp b) {
return new Fp2(this.re.mul(b), this.im.mul(b));
}
Fp2 add(Fp2 b) {
return new Fp2(this.re.add(b.re), this.im.add(b.im));
}
Fp2 sub(Fp2 b) {
return new Fp2(this.re.sub(b.re), this.im.sub(b.im));
}
Fp2 neg() {
return new Fp2(this.re.neg(), this.im.neg());
}
Fp2 inv() {
Fp factor = this.re.square().add(this.im.square()).inv();
return new Fp2(factor.mul(this.re), factor.mul(this.im.neg()));
}
Fp2 mulByNonresidue() {
return new Fp2(this.re.sub(this.im), this.re.add(this.im));
}
Fp2 frobeniusMap(int power) {
return new Fp2(this.re, this.im.mul(FROBENIUS_COEFFICIENTS[power % 2]));
}
void serialize(byte[] out, int off) {
this.im.serialize(out, off);
this.re.serialize(out, Fp.SIZE + off);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Fp2)) {
return false;
}
Fp2 b = (Fp2) o;
return b.re.equals(this.re) && b.im.equals(this.im);
}
}
static class Fp6 {
final static Fp2[] FROBENIUS_COEFFICIENTS_1 = new Fp2[] {
Fp2.ONE,
new Fp2(
Fp.ZERO,
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(Fp.ZERO, Fp.ONE),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
Fp.ZERO,
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe")
),
};
final static Fp2[] FROBENIUS_COEFFICIENTS_2 = new Fp2[] {
Fp2.ONE,
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"),
Fp.ZERO
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa"),
Fp.ZERO
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff"),
Fp.ZERO
),
};
final static Fp6 ZERO = new Fp6(Fp2.ZERO, Fp2.ZERO, Fp2.ZERO);
final static Fp6 ONE = new Fp6(Fp2.ONE, Fp2.ZERO, Fp2.ZERO);
private Fp2 c0;
private Fp2 c1;
private Fp2 c2;
Fp6(Fp2 c0, Fp2 c1, Fp2 c2) {
this.c0 = c0;
this.c1 = c1;
this.c2 = c2;
}
Fp6 add(Fp6 b) {
return new Fp6(this.c0.add(b.c0), this.c1.add(b.c1), this.c2.add(b.c2));
}
Fp6 sub(Fp6 b) {
return new Fp6(this.c0.sub(b.c0), this.c1.sub(b.c1), this.c2.sub(b.c2));
}
Fp6 mul(Fp6 b) {
Fp2 t0 = this.c0.mul(b.c0);
Fp2 t1 = this.c1.mul(b.c1);
Fp2 t2 = this.c2.mul(b.c2);
return new Fp6(
t0.add(this.c1.add(this.c2).mul(b.c1.add(b.c2)).sub(t1.add(t2)).mulByNonresidue()),
c0.add(c1).mul(b.c0.add(b.c1)).sub(t0.add(t1)).add(t2.mulByNonresidue()),
t1.add(c0.add(c2).mul(b.c0.add(b.c2)).sub(t0.add(t2)))
);
}
Fp6 mulByNonresidue() {
return new Fp6(this.c2.mulByNonresidue(), this.c0, this.c1);
}
Fp6 mulByFp2(Fp2 b) {
return new Fp6(this.c0.mul(b), this.c1.mul(b), this.c2.mul(b));
}
Fp6 square() {
Fp2 t0 = this.c0.square();
Fp2 t1 = this.c0.mul(this.c1).mul(2);
Fp2 t3 = this.c1.mul(this.c2).mul(2);
Fp2 t4 = this.c2.square();
return new Fp6(
t3.mulByNonresidue().add(t0),
t4.mulByNonresidue().add(t1),
t1.add(this.c0.sub(this.c1).add(this.c2).square()).add(t3).sub(t0).sub(t4)
);
}
Fp6 neg() {
return new Fp6(this.c0.neg(), this.c1.neg(), this.c2.neg());
}
Fp6 inv() {
Fp2 t0 = this.c0.square().sub(this.c2.mul(this.c1).mulByNonresidue());
Fp2 t1 = this.c2.square().mulByNonresidue().sub(this.c0.mul(this.c1));
Fp2 t2 = this.c1.square().sub(this.c0.mul(this.c2));
Fp2 t4 = this.c2.mul(t1).add(this.c1.mul(t2)).mulByNonresidue().add(this.c0.mul(t0)).inv();
return new Fp6(t4.mul(t0), t4.mul(t1), t4.mul(t2));
}
Fp6 frobeniusMap(int power) {
return new Fp6(
this.c0.frobeniusMap(power),
this.c1.frobeniusMap(power).mul(FROBENIUS_COEFFICIENTS_1[power % 6]),
this.c2.frobeniusMap(power).mul(FROBENIUS_COEFFICIENTS_2[power % 6])
);
}
}
static class Fp12 {
final static Fp2[] FROBENIUS_COEFFICIENTS = new Fp2[] {
Fp2.ONE,
new Fp2(
new Fp("1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8"),
new Fp("00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff"),
Fp.ZERO
),
new Fp2(
new Fp("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2"),
new Fp("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09")
),
new Fp2(
new Fp("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"),
Fp.ZERO
),
new Fp2(
new Fp("144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995"),
new Fp("05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116")
),
new Fp2(
new Fp("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa"),
Fp.ZERO
),
new Fp2(
new Fp("00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3"),
new Fp("1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8")
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac"),
Fp.ZERO
),
new Fp2(
new Fp("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09"),
new Fp("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2")
),
new Fp2(
new Fp("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"),
Fp.ZERO
),
new Fp2(
new Fp("05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116"),
new Fp("144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995")
),
};
final static Fp12 ZERO = new Fp12(Fp6.ZERO, Fp6.ZERO);
final static Fp12 ONE = new Fp12(Fp6.ONE, Fp6.ZERO);
private Fp6 c0;
private Fp6 c1;
Fp12(Fp6 c0, Fp6 c1) {
this.c0 = c0;
this.c1 = c1;
}
Fp12 add(Fp12 b) {
return new Fp12(this.c0.add(b.c0), this.c1.add(b.c1));
}
Fp12 sub(Fp12 b) {
return new Fp12(this.c0.sub(b.c0), this.c1.sub(b.c1));
}
Fp12 mul(Fp12 b) {
Fp6 t1 = this.c0.mul(b.c0);
Fp6 t2 = this.c1.mul(b.c1);
return new Fp12(
t1.add(t2.mulByNonresidue()),
this.c0.add(this.c1).mul(b.c0.add(b.c1)).sub(t1.add(t2))
);
}
Fp12 mulByFp2(Fp2 b) {
return new Fp12(this.c0.mulByFp2(b), this.c1.mulByFp2(b));
}
Fp12 square() {
Fp6 ab = this.c0.mul(this.c1);
return new Fp12(
this.c1.mulByNonresidue().add(this.c0).mul(this.c0.add(this.c1)).sub(ab).sub(ab.mulByNonresidue()),
ab.add(ab)
);
}
Fp12 inv() {
Fp6 t = this.c0.square().sub(this.c1.square().mulByNonresidue()).inv();
return new Fp12(this.c0.mul(t), this.c1.mul(t).neg());
}
Fp12 frobeniusMap(int power) {
Fp6 r0 = this.c0.frobeniusMap(power);
Fp6 r1 = this.c1.frobeniusMap(power);
Fp2 coeff = FROBENIUS_COEFFICIENTS[power % 12];
return new Fp12(
r0,
new Fp6(r1.c0.mul(coeff), r1.c1.mul(coeff), r1.c2.mul(coeff))
);
}
}
static class SqrtDivFp2Res {
private boolean success;
private Fp2 value;
SqrtDivFp2Res(boolean success, Fp2 value) {
this.success = success;
this.value = value;
}
}
static class PointG2 {
final static Fp6 UT_ROOT = new Fp6(Fp2.ZERO, Fp2.ONE, Fp2.ZERO);
final static Fp12 WSQ = new Fp12(UT_ROOT, Fp6.ZERO);
final static Fp12 WCU = new Fp12(Fp6.ZERO, UT_ROOT);
final static Fp12 WSQ_INV = WSQ.inv();
final static Fp12 WCU_INV = WCU.inv();
final static PointG2 ZERO = new PointG2(Fp2.ONE, Fp2.ONE, Fp2.ZERO);
private Fp2 x;
private Fp2 y;
private Fp2 z;
PointG2(Fp2 x, Fp2 y, Fp2 z) {
this.x = x;
this.y = y;
this.z = z;
}
PointG2(byte[] buf) {
this.x = new Fp2(buf, 0);
this.y = new Fp2(buf, Fp2.SIZE);
this.z = Fp2.ONE;
}
PointG2 add(PointG2 b) {
if (this.isZero()) {
return b;
} else if (b.isZero()) {
return this;
}
Fp2 x1 = this.x;
Fp2 y1 = this.y;
Fp2 z1 = this.z;
Fp2 x2 = b.x;
Fp2 y2 = b.y;
Fp2 z2 = b.z;
Fp2 u1 = y2.mul(z1);
Fp2 u2 = y1.mul(z2);
Fp2 v1 = x2.mul(z1);
Fp2 v2 = x1.mul(z2);
if (v1.equals(v2) && u1.equals(u2)) {
return this.doubleP();
}
if (v1.equals(v2)) {
return PointG2.ZERO;
}
Fp2 u = u1.sub(u2);
Fp2 v = v1.sub(v2);
Fp2 vv = v.square();
Fp2 vvv = vv.mul(v);
Fp2 v2vv = v2.mul(vv);
Fp2 w = z1.mul(z2);
Fp2 a = u.square().mul(w).sub(vvv).sub(v2vv.add(v2vv));
Fp2 x3 = v.mul(a);
Fp2 y3 = u.mul(v2vv.sub(a)).sub(vvv.mul(u2));
Fp2 z3 = vvv.mul(w);
return new PointG2(x3, y3, z3);
}
private PointG2 doubleP() {
Fp2 w = this.x.square().mul(3);
Fp2 s = this.y.mul(this.z);
Fp2 ss = s.square();
Fp2 sss = ss.mul(s);
Fp2 b = this.x.mul(this.y).mul(s);
Fp2 h = w.square().sub(b.mul(8));
Fp2 x3 = h.mul(s).mul(2);
Fp2 y3 = w.mul(b.mul(4).sub(h)).sub(
this.y.square().mul(8).mul(ss)
);
Fp2 z3 = sss.mul(8);
return new PointG2(x3, y3, z3);
}
private boolean isZero() {
return this.z.isZero();
}
PointG2 clearCofactor() {
PointG2 t1 = this.mulCurveX();
PointG2 t2 = this.psi();
PointG2 t3 = this.doubleP();
t3 = t3.psi2();
t3 = t3.sub(t2);
t2 = t1.add(t2);
t2 = t2.mulCurveX();
t3 = t3.add(t2);
t3 = t3.sub(t1);
PointG2 q = t3.sub(this);
return q;
}
private PointG2 sub(PointG2 p) {
return this.add(p.neg());
}
private PointG2 neg() {
return new PointG2(x, y.neg(), z);
}
private PointG2 psi2() {
PointG2 p = toAffine();
return new PointG2(p.x.mul(PSI2_C1), p.y.neg(), p.z);
}
private PointG2 psi() {
PointG2 p = toAffine();
Fp2 x2 = WSQ_INV.mulByFp2(p.x).frobeniusMap(1).mul(WSQ).c0.c0;
Fp2 y2 = WCU_INV.mulByFp2(p.y).frobeniusMap(1).mul(WCU).c0.c0;
return new PointG2(x2, y2, p.z);
}
private PointG2 mulCurveX() {
return this.mulUnsafe(CURVE_X).neg();
}
private PointG2 mulUnsafe(BigInteger n) {
PointG2 point = PointG2.ZERO;
PointG2 d = this;
int bitLength = n.bitLength();
for (int i = 0; i < bitLength; i++) {
if (n.testBit(i)) {
point = point.add(d);
}
d = d.doubleP();
}
return point;
}
PointG2 toAffine() {
Fp2 invZ = this.z.inv();
return new PointG2(this.x.mul(invZ), this.y.mul(invZ), Fp2.ONE);
}
byte[] toByteArray(boolean compressed) {
PointG2 p = this.toAffine();
byte[] result = new byte[Fp2.SIZE * (compressed ? 1 : 2)];
p.x.serialize(result, 0);
if (compressed) {
result[0] |= (byte) 0x80;
BigInteger tmp = p.y.im.isZero() ? p.y.re.i.shiftLeft(1) : p.y.im.i.shiftLeft(1);
if (tmp.compareTo(P) > 0) {
result[0] |= 0x20;
}
} else {
p.y.serialize(result, Fp2.SIZE);
}
return result;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof PointG2)) {
return false;
}
PointG2 p = (PointG2) o;
return p.x.equals(this.x) && p.y.equals(this.y) && p.z.equals(this.z);
}
}
}
@@ -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,68 @@
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.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, byte p2) throws IOException {
APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_SIGN, 0x00, p2, data);
return apduChannel.send(sign);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash with ECDSA 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 {
return sign(data, KeycardCommandSet.SIGN_P2_ECDSA);
}
/**
* Sends a SIGN APDU. The message can be any length, and it is mapped to a point on G2 internally.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signBLS(byte[] data) throws IOException {
return sign(BLS.hash(data), KeycardCommandSet.SIGN_P2_BLS12_381);
}
}
@@ -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;
@@ -53,6 +54,18 @@ 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 SIGN_P2_ECDSA = 0x00;
public static final byte SIGN_P2_BLS12_381 = 0x01;
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;
@@ -154,8 +167,9 @@ public class KeycardCommandSet {
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException secure channel error
*/
public void autoOpenSecureChannel() throws IOException {
public void autoOpenSecureChannel() throws IOException, APDUException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
@@ -163,8 +177,9 @@ public class KeycardCommandSet {
* Automatically pairs. Derives the secret from the given password.
*
* @throws IOException communication error
* @throws APDUException pairing error
*/
public void autoPair(String pairingPassword) throws IOException {
public void autoPair(String pairingPassword) throws IOException, APDUException {
byte[] secret = pairingPasswordToSecret(pairingPassword);
secureChannel.autoPair(apduChannel, secret);
@@ -193,8 +208,9 @@ public class KeycardCommandSet {
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException pairing error
*/
public void autoPair(byte[] sharedSecret) throws IOException {
public void autoPair(byte[] sharedSecret) throws IOException, APDUException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
@@ -202,8 +218,9 @@ public class KeycardCommandSet {
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void autoUnpair() throws IOException {
public void autoUnpair() throws IOException, APDUException {
secureChannel.autoUnpair(apduChannel);
}
@@ -244,6 +261,9 @@ public class KeycardCommandSet {
/**
* Unpair all other clients.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
@@ -262,19 +282,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.
*
@@ -482,65 +490,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);
}
@@ -581,6 +581,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.
*
@@ -588,7 +615,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);
}
@@ -648,7 +675,56 @@ public class KeycardCommandSet {
}
/**
* Sends the INIT command to the card.
* 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. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
* @param puk the PUK
@@ -657,7 +733,22 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
return this.init(pin, puk, pairingPasswordToSecret(pairingPassword));
return this.init(pin, puk, pairingPassword, (byte) 0, (byte) 0);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param puk the PUK
* @param pairingPassword pairing password
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
return this.init(pin, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
/**
@@ -670,9 +761,31 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length);
return init(pin, puk, sharedSecret, (byte) 0, (byte) 0);
}
/**
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
* @param puk the PUK
* @param sharedSecret the shared secret for pairing
* @param pinRetries the number of allowed PIN retries
* @param pukRetries the number of allowed PUK retries
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException {
boolean addRetries = !((pinRetries == 0) || (pukRetries == 0));
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length + (addRetries ? 2 : 0));
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
if (addRetries) {
initData[initData.length - 2] = pinRetries;
initData[initData.length - 1] = pukRetries;
}
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
@@ -0,0 +1,106 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.SortedSet;
import java.util.TreeSet;
public class Metadata {
private String cardName;
private SortedSet<Long> wallets;
public static Metadata fromData(byte[] data) {
int version = (data[0] & 0xe0) >> 5;
if (version != 1) {
throw new RuntimeException("Invalid version");
}
int namelen = (data[0] & 0x1f);
int off = 1;
String cardName = new String(data, off, namelen, Charset.forName("US-ASCII"));
off += namelen;
SortedSet<Long> set = new TreeSet<>();
while(off < data.length) {
int[] start = TinyBERTLV.readNum(data, off);
int[] count = TinyBERTLV.readNum(data, start[1]);
off = count[1];
long s = start[0] & 0xffffffffl;
buildRange(set, s, (s + count[0]));
}
return new Metadata(cardName, set);
}
private static void buildRange(SortedSet<Long> set, long start, long end) {
for (long i = start; i <= end; i++) {
set.add(i);
}
}
Metadata(String cardName, SortedSet<Long> wallets) {
this.cardName = cardName;
this.wallets = wallets;
}
public Metadata(String cardName) {
this(cardName, new TreeSet<>());
}
public String getCardName() {
return cardName;
}
public void setCardName(String cardName) {
if (cardName.length() > 20) {
throw new IllegalArgumentException("card name too long");
}
this.cardName = cardName;
}
public SortedSet<Long> getWallets() {
return wallets;
}
public void addWallet(long w) {
this.wallets.add(w);
}
public void removeWallet(long w) {
this.wallets.remove(w);
}
public byte[] toByteArray() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] name = this.cardName.getBytes(Charset.forName("US-ASCII"));
os.write(0x20 | name.length);
os.write(name, 0, name.length);
if (wallets.isEmpty()) {
return os.toByteArray();
}
long start = wallets.first();
int len = 0;
for (Long w : wallets.tailSet(start + 1)) {
if (w == (start + len + 1)) {
len++;
} else {
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
len = 0;
start = w;
}
}
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
return os.toByteArray();
}
}
@@ -3,10 +3,6 @@ package im.status.keycard.applet;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Scanner;
public class Mnemonic {
private final static int WORDLIST_SIZE = 2048;
@@ -41,28 +37,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;
}
/**
@@ -145,7 +124,7 @@ public class Mnemonic {
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
throw new RuntimeException("Is Bouncycastle correctly initialized?", e);
}
return key.getEncoded();
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);
@@ -118,24 +118,14 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException, APDUException {
APDUResponse response = openSecureChannel(apduChannel, pairing.getPairingIndex(), publicKey);
if (response.getSw() != 0x9000) {
throw new IOException("OPEN SECURE CHANNEL failed");
}
response.checkOK("OPEN SECURE CHANNEL failed");
processOpenSecureChannelResponse(response);
response = mutuallyAuthenticate(apduChannel);
if (response.getSw() != 0x9000) {
throw new IOException("MUTUALLY AUTHENTICATE failed");
}
if(!verifyMutuallyAuthenticateResponse(response)) {
throw new IOException("Invalid authentication data from the card");
}
response.checkOK("MUTUALLY AUTHENTICATE failed");
verifyMutuallyAuthenticateResponse(response);
}
/**
@@ -168,8 +158,10 @@ public class SecureChannelSession {
* @param response the card response
* @return true if response is correct, false otherwise
*/
public boolean verifyMutuallyAuthenticateResponse(APDUResponse response) {
return response.getData().length == SC_SECRET_LENGTH;
public void verifyMutuallyAuthenticateResponse(APDUResponse response) throws APDUException {
if (response.getData().length != SC_SECRET_LENGTH) {
throw new APDUException("Invalid authentication data from the card");
}
}
/**
@@ -178,14 +170,10 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException {
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException, APDUException {
byte[] challenge = new byte[32];
random.nextBytes(challenge);
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 1");
}
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge).checkOK("Pairing failed on step 1");
byte[] respData = resp.getData();
byte[] cardCryptogram = Arrays.copyOf(respData, 32);
@@ -204,18 +192,13 @@ public class SecureChannelSession {
checkCryptogram = md.digest(challenge);
if (!Arrays.equals(checkCryptogram, cardCryptogram)) {
throw new IOException("Invalid card cryptogram");
throw new APDUException("Invalid card cryptogram");
}
md.update(sharedSecret);
checkCryptogram = md.digest(cardChallenge);
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 2");
}
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram).checkOK("Pairing failed on step 2");
respData = resp.getData();
md.update(sharedSecret);
pairing = new Pairing(md.digest(Arrays.copyOfRange(respData, 1, respData.length)), respData[0]);
@@ -227,12 +210,8 @@ public class SecureChannelSession {
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoUnpair(CardChannel apduChannel) throws IOException {
APDUResponse resp = unpair(apduChannel, pairing.getPairingIndex());
if (resp.getSw() != 0x9000) {
throw new IOException("Unpairing failed");
}
public void autoUnpair(CardChannel apduChannel) throws IOException, APDUException {
unpair(apduChannel, pairing.getPairingIndex()).checkOK("Unpairing failed");
}
/**
@@ -1,5 +1,6 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/**
@@ -14,6 +15,57 @@ public class TinyBERTLV {
private byte[] buffer;
private int pos;
public static int[] readNum(byte[] buf, int off) {
int len = buf[off++] & 0xff;
int lenlen = 0;
if ((len & 0x80) == 0x80) {
lenlen = len & 0x7f;
len = readVal(buf, off, lenlen);
}
return new int[] {len, off + lenlen};
}
public static int readVal(byte[] val, int off, int len) {
switch (len) {
case 1:
return val[off] & 0xff;
case 2:
return ((val[off] & 0xff) << 8) | (val[off+1] & 0xff);
case 3:
return ((val[off] & 0xff) << 16) | ((val[off+1] & 0xff) << 8) | (val[off+2] & 0xff);
case 4:
return ((val[off] & 0xff) << 24) | ((val[off+1] & 0xff) << 16) | ((val[off+2] & 0xff) << 8) | (val[off+3] & 0xff);
default:
throw new IllegalArgumentException("Integers of length " + len + " are unsupported");
}
}
public static void writeNum(ByteArrayOutputStream os, int len) {
if ((len & 0xff000000) != 0) {
os.write(0x84);
os.write((len & 0xff000000) >> 24);
os.write((len & 0x00ff0000) >> 16);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x00ff0000) != 0) {
os.write(0x83);
os.write((len & 0x00ff0000) >> 16);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x0000ff00) != 0) {
os.write(0x82);
os.write((len & 0x0000ff00) >> 8);
os.write(len & 0x000000ff);
} else if ((len & 0x00000080) != 0) {
os.write(0x81);
os.write(len & 0x000000ff);
} else {
os.write(len);
}
}
public TinyBERTLV(byte[] buffer) {
this.buffer = buffer;
this.pos = 0;
@@ -64,19 +116,7 @@ public class TinyBERTLV {
*/
public int readInt() throws IllegalArgumentException {
byte[] val = readPrimitive(TLV_INT);
switch (val.length) {
case 1:
return val[0] & 0xff;
case 2:
return ((val[0] & 0xff) << 8) | (val[1] & 0xff);
case 3:
return ((val[0] & 0xff) << 16) | ((val[1] & 0xff) << 8) | (val[2] & 0xff);
case 4:
return ((val[0] & 0xff) << 24) | ((val[1] & 0xff) << 16) | ((val[2] & 0xff) << 8) | (val[3] & 0xff);
default:
throw new IllegalArgumentException("Integers of length " + val.length + " are unsupported");
}
return TinyBERTLV.readVal(val, 0, val.length);
}
/**
@@ -104,13 +144,9 @@ public class TinyBERTLV {
* @return the tag
*/
public int readLength() {
int len = buffer[pos++] & 0xff;
if (len == 0x81) {
len = buffer[pos++] & 0xff;
}
return len;
int[] len = TinyBERTLV.readNum(buffer, pos);
pos = len[1];
return len[0];
}
private void checkTag(int expected, int actual) throws IllegalArgumentException {
@@ -61,7 +61,7 @@ public class Crypto {
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
} catch (NoSuchProviderException e) {
throw new RuntimeException("SpongyCastle not installed");
throw new RuntimeException("BouncyCastle not installed");
}
}
@@ -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;
}
}
@@ -88,6 +88,35 @@ public class APDUResponse {
}
}
/**
* Asserts that the SW is 0x9000. Throws an exception with the given message if it isn't
*
* @param message the error message
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkOK(String message) throws APDUException {
return checkSW(message, SW_OK);
}
/**
* Asserts that the SW is contained in the given list. Throws an exception with the given message if it isn't.
*
* @param message the error message
* @param codes the list of SWs to match.
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkSW(String message, int... codes) throws APDUException {
for (int code : codes) {
if (this.sw == code) {
return this;
}
}
throw new APDUException(this.sw, message);
}
/**
* Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
*
@@ -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);
}
}