Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c424eed6 | ||
|
|
bbcce01742 | ||
|
|
a39924aba3 | ||
|
|
22db82e8f2 | ||
|
|
9295aa6553 | ||
|
|
86e6cb60ec | ||
|
|
aaf2c9d9e6 | ||
|
|
9431d7c497 | ||
|
|
f97363704b | ||
|
|
144474415d | ||
|
|
3f8966f1a8 | ||
|
|
3acea10750 | ||
|
|
8cb43e6717 | ||
|
|
6bf8da8374 | ||
|
|
536bad2671 | ||
|
|
b1be261ea1 | ||
|
|
4a69788473 | ||
|
|
eed0b09fed |
@@ -1,8 +1,11 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.vscode
|
||||
/local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/desktop/bin
|
||||
lib/bin
|
||||
.externalNativeBuild
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -15,7 +15,7 @@ You can import the SDK in your Gradle or Maven project using [Jitpack.io](https:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'com.github.status-im.status-keycard-java:android:2.2.0'
|
||||
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.2.0'
|
||||
implementation 'com.github.status-im.status-keycard-java:desktop:3.0.2'
|
||||
}
|
||||
```
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 201
|
||||
versionName "2.0.1"
|
||||
versionCode 304
|
||||
versionName "3.0.4"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -58,6 +59,13 @@ public class KeycardCommandSet {
|
||||
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;
|
||||
@@ -159,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);
|
||||
}
|
||||
|
||||
@@ -168,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);
|
||||
@@ -198,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);
|
||||
}
|
||||
|
||||
@@ -207,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);
|
||||
}
|
||||
|
||||
@@ -249,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);
|
||||
@@ -267,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.
|
||||
*
|
||||
@@ -486,57 +489,6 @@ public class KeycardCommandSet {
|
||||
return secureChannel.transmit(apduChannel, generateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 00, P2 to the entropy count and the data is the first entropy piece.
|
||||
* This starts a duplication session. Requires an open Secure Channel and authenticated PIN.
|
||||
*
|
||||
* @param entropyCount the number of entropy pieces to expect, including the one in this APDU
|
||||
* @param firstEntropy a random 32-byte number
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyStart(int entropyCount, byte[] firstEntropy) throws IOException {
|
||||
APDUCommand duplicateKeyStart = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_START, entropyCount, firstEntropy);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 01 and the data is the entropy. This adds entropy and does not require
|
||||
* a Secure Channel or authenticated PIN.
|
||||
*
|
||||
* @param entropy a random 32-byte number
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyAddEntropy(byte[] entropy) throws IOException {
|
||||
APDUCommand duplicateKeyAddEntropy = new APDUCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_ADD_ENTROPY, 0, secureChannel.oneShotEncrypt(entropy));
|
||||
return apduChannel.send(duplicateKeyAddEntropy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 02. This exports the encrypted master key including chaining code.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyExport() throws IOException {
|
||||
APDUCommand duplicateKeyExport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_EXPORT, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyExport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DUPLICATE KEY APDU. The P1 is set to 03. This imports an encrypted master key including chaining code. The
|
||||
* response data contains the key UID of the imported key.
|
||||
*
|
||||
* @param key the key, exported from another card in the same duplication session.
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse duplicateKeyImport(byte[] key) throws IOException {
|
||||
APDUCommand duplicateKeyImport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_IMPORT, 0, key);
|
||||
return secureChannel.transmit(apduChannel, duplicateKeyImport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
|
||||
*
|
||||
@@ -578,7 +530,6 @@ public class KeycardCommandSet {
|
||||
return sign(hash, SIGN_P1_PINLESS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
|
||||
* a derivation path.
|
||||
@@ -724,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
|
||||
@@ -733,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -746,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,16 +117,9 @@ public class Mnemonic {
|
||||
}
|
||||
|
||||
public static byte[] toBinarySeed(String mnemonicPhrase, String password) {
|
||||
SecretKey key;
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512", "BC");
|
||||
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?");
|
||||
}
|
||||
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512", "BC");
|
||||
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
|
||||
SecretKey key = skf.generateSecret(spec);
|
||||
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 {
|
||||
|
||||
@@ -31,6 +31,10 @@ public class Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
Crypto.addBouncyCastleProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a session key for SCP02.
|
||||
*
|
||||
@@ -61,7 +65,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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,16 @@ public class GlobalPlatformCommandSet {
|
||||
return delete(Identifiers.getKeycardInstanceAID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Keycard Cash applet instance.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deleteCashInstance() throws IOException {
|
||||
return delete(Identifiers.CASH_INSTANCE_AID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the NDEF applet instance.
|
||||
*
|
||||
@@ -260,6 +270,7 @@ public class GlobalPlatformCommandSet {
|
||||
public void deleteKeycardInstancesAndPackage() throws IOException, APDUException {
|
||||
deleteNDEFInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteKeycardInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteCashInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -416,4 +427,25 @@ public class GlobalPlatformCommandSet {
|
||||
public APDUResponse installKeycardApplet() throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.KEYCARD_AID, Identifiers.getKeycardInstanceAID(), new byte[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Cash applet.
|
||||
*
|
||||
* @param cashData the initial Cash data. Can be a zero-length array but not null
|
||||
* @return the card response
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse installCashApplet(byte[] cashData) throws IOException {
|
||||
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.CASH_AID, Identifiers.CASH_INSTANCE_AID, cashData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Cash applet.
|
||||
*
|
||||
* @return the card response
|
||||
* @throws IOException communication error.
|
||||
*/
|
||||
public APDUResponse installCashApplet() throws IOException {
|
||||
return installCashApplet(new byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user