Compare commits

..
Author SHA1 Message Date
Michele Balistreri 9e0f11f71c chain code in pubkeys 2022-11-14 09:20:59 +01:00
Michele Balistreri 64aece4837 init with alt pin 2022-11-10 13:49:06 +01:00
Michele Balistreri 7d968cf969 support export chain code (#28) 2022-11-10 08:11:03 +01:00
Michele Balistreri 9fac06b19d Add IDENTIFY CARD (#24)
* ident applet support

* add identify card command

* fix certificate class

* make sure private key is 32 bytes

* don't remove TLV header from signature

* fix typo

* use secure channel if open
2022-11-04 12:33:06 +01:00
Audrius Molis 6c965f726a test adding exception info to the RuntimeException in RuntimeException (#26) 2022-09-09 15:31:58 +02:00
Michele Balistreri bbcce01742 add metadata parser/encoder (#22) 2022-07-18 14:16:36 +02:00
Michele Balistreri a39924aba3 BLS support (#21)
add support for BLS
2022-07-18 14:05:43 +02:00
Michele Balistreri 22db82e8f2 add init with pin/puk retries 2021-12-23 09:29:21 +03:00
Michele Balistreri 9295aa6553 check OPEN SECURE CHANNEL response 2020-11-09 15:36:50 +01:00
Michele Balistreri 86e6cb60ec bc 1.60 2020-11-09 14:03:25 +01:00
Michele Balistreri aaf2c9d9e6 update README 2020-06-02 10:22:27 +03:00
Michele Balistreri 9431d7c497 differentiate between communication errors and unexpected APDU response in the "auto" methods of the SecureChannelSession 2020-06-02 09:44:11 +03:00
Michele Balistreri f97363704b Merge branch 'master' of github.com:status-im/status-keycard-java 2020-04-15 13:52:10 +03:00
Michele Balistreri 144474415d closes #20 2020-04-15 13:46:49 +03:00
ligi 3f8966f1a8 Make setNDEF backward compatible to the 2.x style (#19) 2019-10-23 14:21:12 +03:00
Michele Balistreri 3acea10750 hardcode english dictionary 2019-10-23 09:59:44 +03:00
Michele Balistreri 8cb43e6717 fix typo 2019-10-16 14:46:06 +03:00
Michele Balistreri 6bf8da8374 add generic STORE DATA/GET DATA method, reimplement setNDEF 2019-10-16 14:11:41 +03:00
Michele Balistreri 536bad2671 add data to cash applet 2019-10-16 13:30:21 +03:00
Michele Balistreri b1be261ea1 remove DUPLICATE KEY command 2019-10-15 14:55:48 +03:00
Bitgamma 4a69788473 V2.3 (#18)
* add STORE/GET DATA commands

* add basic CashCommandSet and installation methods

* remove P1

* add CashApplicationInfo

* add Nano X support

* (unfinished) BLE support

* refactor ledger protocol support for better code reuse

* remove spurious declaration

* finish ble implementation

* enable notifications

* fixed segmentation over BLE

* update GlobalPlatform's delete method
2019-09-02 13:40:06 +03:00
Bitgamma eed0b09fed Create LICENSE 2019-04-29 09:26:17 +03:00
Michele Balistreri 7ce0136b1d auto upgrade to new keys 2019-04-05 16:09:57 +03:00
Michele Balistreri 2865d2a08a use key identifier 1 2019-04-05 13:26:58 +03:00
Michele Balistreri 3e5bb577d7 indicate multiple keys in P2 2019-04-05 13:17:11 +03:00
Michele Balistreri e882e39105 use correct encryption scheme 2019-04-05 12:44:05 +03:00
Michele Balistreri 15a2c43a70 actually save the DEK key 2019-04-05 12:38:05 +03:00
Michele Balistreri 205f150705 add PUT KEY command 2019-04-05 12:22:00 +03:00
Bitgamma a4ff736d6e Keycard v2.2 (#15)
* add methods for the extended SIGN command

* add resetPinlessPath
2019-04-04 10:20:38 +03:00
Michele Balistreri cd7c4ba3bc Merge remote-tracking branch 'origin/master' 2019-03-27 19:04:14 +03:00
Michele Balistreri 152d7f8e34 update documentation 2019-03-27 19:03:09 +03:00
ligi ee41722527 Only ignore specific lint issue (#14)
while  f4dd1d1 fixes #13 it is very broadly deactivating lint trowing errors
This PR makes it just ignore the specific lint problem at hand
2019-03-27 18:38:39 +03:00
Michele Balistreri f4dd1d17cf closes #13 2019-03-27 13:08:02 +03:00
Michele Balistreri 425d085716 correct value for Ledger 2019-03-20 13:24:22 +03:00
Michele Balistreri b249bd75e6 make the iteration count for PBKDF2 configurable per-device 2019-03-20 13:18:24 +03:00
Michele Balistreri f603979cb5 fix sending commands longer than 64 bytes 2019-03-18 11:54:15 +03:00
Michele Balistreri db3eee0d76 make r and s unsigned 2019-03-14 12:56:32 +03:00
Michele Balistreri 99dbb24067 increase read timeout 2019-02-27 10:52:36 +03:00
Michele Balistreri f9df2be80f add implicit capabilities of non-initialized devices 2019-02-26 14:23:48 +03:00
Michele Balistreri 07cba4aa9d support devices without SecureChannel capability 2019-02-26 12:07:42 +03:00
Michele Balistreri 527efc7a4e add initial USB connector 2019-02-25 16:30:49 +03:00
Michele Balistreri 067204c7db update demo 2019-02-13 12:22:09 +03:00
Michele Balistreri 2df15f388c add capability parsing 2019-02-12 14:38:24 +03:00
Michele Balistreri 93d71ab70b keep track of used cards 2019-01-15 17:00:01 +03:00
Michele Balistreri 50fa93dd32 change the convenience constructor 2019-01-15 11:30:12 +03:00
Michele Balistreri bbe3693e4e implement card duplication 2019-01-14 18:23:52 +03:00
Michele Balistreri b5ff6d033e simplify PIN verification 2019-01-08 15:15:18 +03:00
Michele Balistreri 93ff092a14 add utility credentials change methods 2019-01-08 14:23:10 +03:00
Michele Balistreri f6f9a65be1 remove spongycastle dependency 2018-12-18 12:12:08 +03:00
Michele Balistreri 0d16541875 2.0.0 release 2018-12-14 13:49:59 +03:00
Michele Balistreri 171220bd79 change AIDs 2018-12-14 13:21:36 +03:00
42 changed files with 5153 additions and 338 deletions
+3
View File
@@ -1,8 +1,11 @@
*.iml
.gradle
.vscode
/local.properties
.idea
.DS_Store
/build
/captures
/desktop/bin
lib/bin
.externalNativeBuild
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+2 -2
View File
@@ -15,7 +15,7 @@ You can import the SDK in your Gradle or Maven project using [Jitpack.io](https:
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:android:2.0rc1'
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.0rc1'
implementation 'com.github.status-im.status-keycard-java:desktop:3.0.2'
}
```
+11 -2
View File
@@ -8,8 +8,17 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "2.0"
versionCode 304
versionName "3.0.4"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
}
+6 -1
View File
@@ -4,6 +4,11 @@
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
</manifest>
@@ -0,0 +1,169 @@
package im.status.keycard.android;
import android.bluetooth.*;
import android.content.Context;
import im.status.keycard.io.*;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class LedgerBLEChannel implements CardChannel {
final public static UUID LEDGER_UUID = UUID.fromString("13D63400-2C97-0004-0000-4C6564676572");
final public static UUID LEDGER_REQ_UUID = UUID.fromString("13D63400-2C97-0004-0002-4C6564676572");
final public static UUID LEDGER_RSP_UUID = UUID.fromString("13D63400-2C97-0004-0001-4C6564676572");
final private static int BLE_WRITE_FAILED = -1;
final private static int BLE_WRITE_STARTED = 0;
final private static int BLE_WRITE_FINISHED = 1;
final private static int BLE_TIMEOUT = 2000;
final private BluetoothGatt bluetoothGatt;
private BluetoothGattCharacteristic reqChar;
private boolean connected;
private int mtuSize;
private int writeStatus;
private LinkedBlockingQueue<byte[]> readQueue;
public LedgerBLEChannel(Context context, BluetoothDevice device, CardListener listener) {
this.connected = false;
this.mtuSize = 20;
this.readQueue = new LinkedBlockingQueue<>();
this.writeStatus = BLE_WRITE_FINISHED;
final CardChannel channel = this;
this.bluetoothGatt = device.connectGatt(context, false, new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (connected == (newState == BluetoothProfile.STATE_CONNECTED)) {
return;
}
connected = newState == BluetoothProfile.STATE_CONNECTED;
if (connected) {
bluetoothGatt.discoverServices();
} else {
(new Thread() {
@Override
public void run() {
listener.onDisconnected();
}
}).start();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
BluetoothGattService service = bluetoothGatt.getService(LEDGER_UUID);
if (service == null) {
bluetoothGatt.disconnect();
connected = false;
return;
}
reqChar = service.getCharacteristic(LEDGER_REQ_UUID);
BluetoothGattCharacteristic rsp = service.getCharacteristic(LEDGER_RSP_UUID);
bluetoothGatt.setCharacteristicNotification(rsp, true);
BluetoothGattDescriptor rspDesc = rsp.getDescriptors().get(0);
rspDesc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(rspDesc);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
writeStatus = status == BluetoothGatt.GATT_SUCCESS ? BLE_WRITE_FINISHED : BLE_WRITE_FAILED;
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
reqChar.setValue(new byte[] {0x08, 0x00, 0x00, 0x00, 0x00});
bluetoothGatt.writeCharacteristic(reqChar);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] rsp = characteristic.getValue();
if (rsp[0] == 0x08) {
mtuSize = rsp[5];
(new Thread() {
@Override
public void run() {
listener.onConnected(channel);
}
}).start();
return;
}
readQueue.offer(rsp);
}
});
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
return LedgerUtil.send(cmd, mtuSize, false, new LedgerUtil.Callback() {
@Override
public void write(byte[] chunk) throws IOException {
writeStatus = BLE_WRITE_STARTED;
reqChar.setValue(chunk);
bluetoothGatt.writeCharacteristic(reqChar);
long timeout = 0;
while(writeStatus == BLE_WRITE_STARTED || timeout >= BLE_TIMEOUT) {
try {
Thread.sleep(10);
timeout += 10;
} catch (InterruptedException e) {
throw new IOException("write interrupted");
}
}
if (writeStatus != BLE_WRITE_FINISHED) {
throw new IOException("write operation failed");
}
}
@Override
public void read(byte[] chunk) throws IOException {
try {
byte[] data = readQueue.poll(BLE_TIMEOUT, TimeUnit.MILLISECONDS);
if (data == null) {
throw new IOException("read timeout");
}
System.arraycopy(data, 0, chunk, 0, Math.min(data.length, chunk.length));
} catch (InterruptedException e) {
throw new IOException("read timeout");
}
}
});
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public int pairingPasswordPBKDF2IterationCount() {
return 10;
}
public void close() {
bluetoothGatt.close();
}
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
}
@@ -0,0 +1,90 @@
package im.status.keycard.android;
import android.app.Activity;
import android.bluetooth.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import im.status.keycard.globalplatform.Crypto;
import im.status.keycard.io.CardListener;
import java.util.UUID;
public class LedgerBLEManager {
private static final int REQUEST_ENABLE_BT = 1;
final private BluetoothAdapter bluetoothAdapter;
final private Activity activity;
private CardListener cardListener;
static {
Crypto.addBouncyCastleProvider();
}
public LedgerBLEManager(Activity context) {
this.activity = context;
final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
this.bluetoothAdapter = bluetoothManager.getAdapter();
}
public void ensureBLEEnabled() {
if (!bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
public void startScan(BluetoothAdapter.LeScanCallback cb) {
bluetoothAdapter.startLeScan(new UUID[] { LedgerBLEChannel.LEDGER_UUID}, cb);
}
public void stopScan(BluetoothAdapter.LeScanCallback cb) {
bluetoothAdapter.stopLeScan(cb);
}
public void connectDevice(BluetoothDevice device) {
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
final IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
activity.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final BluetoothDevice d = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
if (!d.getAddress().equals(device.getAddress())) {
return;
}
if (bondState == BluetoothDevice.BOND_BONDED) {
activity.unregisterReceiver(this);
// connect/disconnect to make bond permanent
device.connectGatt(activity, false, new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
gatt.disconnect();
onConnected(device);
}
}
});
}
}
}, filter);
device.createBond();
} else {
onConnected(device);
}
}
private void onConnected(BluetoothDevice device) {
if (cardListener != null) {
new LedgerBLEChannel(activity, device, cardListener);
}
}
public void setCardListener(CardListener cardListener) {
this.cardListener = cardListener;
}
}
@@ -24,7 +24,7 @@ public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback
private int loopSleepMS;
static {
Crypto.addSpongyCastleProvider();
Crypto.addBouncyCastleProvider();
}
/**
+4
View File
@@ -22,3 +22,7 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}
subprojects {
tasks.withType(Javadoc).all { enabled = false }
}
+12 -4
View File
@@ -6,8 +6,8 @@ android {
applicationId "im.status.keycard.demo"
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "2.0"
versionCode 300
versionName "3.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -16,13 +16,21 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
lintConfig file("lint-config.xml")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.madgag.spongycastle:prov:1.58.0.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
implementation project(':android')
+5
View File
@@ -0,0 +1,5 @@
<lint>
<issue id="InvalidPackage">
<ignore path="**/bcprov-jdk15on-*.jar"/>
</issue>
</lint>
@@ -1,15 +1,18 @@
package im.status.keycard.app;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import im.status.keycard.android.LedgerBLEManager;
import im.status.keycard.demo.R;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.CardListener;
import im.status.keycard.android.NFCCardManager;
import im.status.keycard.applet.*;
import org.spongycastle.util.encoders.Hex;
import org.bouncycastle.util.encoders.Hex;
public class MainActivity extends AppCompatActivity {
@@ -17,6 +20,8 @@ public class MainActivity extends AppCompatActivity {
private NfcAdapter nfcAdapter;
private NFCCardManager cardManager;
//private LedgerBLEManager cardManager;
//private boolean connected;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -24,7 +29,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(R.layout.activity_main);
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
cardManager = new NFCCardManager();
//cardManager = new LedgerBLEManager(this);
cardManager.setCardListener(new CardListener() {
@Override
public void onConnected(CardChannel cardChannel) {
@@ -54,20 +59,27 @@ public class MainActivity extends AppCompatActivity {
} else {
Log.i(TAG, "The card has no master key");
}
Log.i(TAG, String.format("Capabilities: %02X", info.getCapabilities()));
Log.i(TAG, "Has Secure Channel: " + info.hasSecureChannelCapability());
Log.i(TAG, "Has Key Management: " + info.hasKeyManagementCapability());
Log.i(TAG, "Has Credentials Management: " + info.hasCredentialsManagementCapability());
Log.i(TAG, "Has NDEF capability: " + info.hasNDEFCapability());
// In real projects, the pairing key should be saved and used for all new sessions.
cmdSet.autoPair("KeycardTest");
Pairing pairing = cmdSet.getPairing();
if (info.hasSecureChannelCapability()) {
// In real projects, the pairing key should be saved and used for all new sessions.
cmdSet.autoPair("KeycardTest");
Pairing pairing = cmdSet.getPairing();
// Never log the pairing key in a real application!
Log.i(TAG, "Pairing with card is done.");
Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
// Never log the pairing key in a real application!
Log.i(TAG, "Pairing with card is done.");
Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
// Opening a Secure Channel is needed for all other applet commands
cmdSet.autoOpenSecureChannel();
// Opening a Secure Channel is needed for all other applet commands
cmdSet.autoOpenSecureChannel();
Log.i(TAG, "Secure channel opened. Getting applet status.");
Log.i(TAG, "Secure channel opened. Getting applet status.");
}
// We send a GET STATUS command, which does not require PIN authentication
ApplicationStatus status = new ApplicationStatus(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_APPLICATION).checkOK().getData());
@@ -76,27 +88,31 @@ public class MainActivity extends AppCompatActivity {
Log.i(TAG, "PUK retry counter: " + status.getPUKRetryCount());
Log.i(TAG, "Has master key: " + status.hasMasterKey());
// A mnemonic can be generated before PIN authentication. Generating a mnemonic does not create keys on the
// card. a subsequent loadKey step must be performed after PIN authentication. In this example we will only
// show how to convert the output of the card to a usable format but won't actually load the key
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(KeycardCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
if (info.hasKeyManagementCapability()) {
// A mnemonic can be generated before PIN authentication. Generating a mnemonic does not create keys on the
// card. a subsequent loadKey step must be performed after PIN authentication. In this example we will only
// show how to convert the output of the card to a usable format but won't actually load the key
Mnemonic mnemonic = new Mnemonic(cmdSet.generateMnemonic(KeycardCommandSet.GENERATE_MNEMONIC_12_WORDS).checkOK().getData());
// We need to set a wordlist if we plan using this object to derive the binary seed. If we just need the word
// indexes we can skip this step and call mnemonic.getIndexes() instead.
mnemonic.fetchBIP39EnglishWordlist();
// We need to set a wordlist if we plan using this object to derive the binary seed. If we just need the word
// indexes we can skip this step and call mnemonic.getIndexes() instead.
mnemonic.fetchBIP39EnglishWordlist();
Log.i(TAG, "Generated mnemonic phrase: " + mnemonic.toMnemonicPhrase());
Log.i(TAG, "Binary seed: " + Hex.toHexString(mnemonic.toBinarySeed()));
Log.i(TAG, "Generated mnemonic phrase: " + mnemonic.toMnemonicPhrase());
Log.i(TAG, "Binary seed: " + Hex.toHexString(mnemonic.toBinarySeed()));
}
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkOK();
if (info.hasCredentialsManagementCapability()) {
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkAuthOK();
Log.i(TAG, "Pin Verified.");
Log.i(TAG, "Pin Verified.");
}
// If the card has no keys, we generate a new set. Keys can also be loaded on the card starting from a binary
// seed generated from a mnemonic phrase. In alternative, we could load the generated keypair as shown in the
// commented line of code.
if (!status.hasMasterKey()) {
if (!status.hasMasterKey() && info.hasKeyManagementCapability()) {
cmdSet.generateKey();
//cmdSet.loadKey(mnemonic.toBIP32KeyPair());
}
@@ -127,14 +143,15 @@ public class MainActivity extends AppCompatActivity {
Log.i(TAG, "R: " + Hex.toHexString(signature.getR()));
Log.i(TAG, "S: " + Hex.toHexString(signature.getS()));
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
// this would be a very bad idea to do.
cmdSet.unpairOthers();
cmdSet.autoUnpair();
Log.i(TAG, "Unpaired.");
if (info.hasSecureChannelCapability()) {
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
// this would be a very bad idea to do.
cmdSet.unpairOthers();
cmdSet.autoUnpair();
Log.i(TAG, "Unpaired.");
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
@@ -147,6 +164,19 @@ public class MainActivity extends AppCompatActivity {
}
});
cardManager.start();
/*connected = false;
cardManager.startScan(new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
if (connected) {
return;
}
connected = true;
cardManager.stopScan(this);
cardManager.connectDevice(device);
}
});*/
}
@Override
+15
View File
@@ -1,8 +1,23 @@
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')
compile 'org.hid4java:hid4java:0.5.0'
}
task sourcesJar(type: Jar, dependsOn: classes) {
@@ -0,0 +1,49 @@
package im.status.keycard.desktop;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.LedgerUtil;
import org.hid4java.HidDevice;
import java.io.IOException;
public class LedgerUSBChannel implements CardChannel {
private static final int HID_BUFFER_SIZE = 64;
private static final int READ_TIMEOUT = 20000;
private HidDevice hidDevice;
public LedgerUSBChannel(HidDevice hidDevice) {
this.hidDevice = hidDevice;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
return LedgerUtil.send(cmd, HID_BUFFER_SIZE, true, new LedgerUtil.Callback() {
@Override
public void write(byte[] chunk) throws IOException {
if (hidDevice.write(chunk, chunk.length, (byte) 0x00) < 0) {
throw new IOException("Write failed");
}
}
@Override
public void read(byte[] chunk) throws IOException {
if (hidDevice.read(chunk, READ_TIMEOUT) < 0) {
throw new IOException("Read failed");
}
}
});
}
@Override
public boolean isConnected() {
return hidDevice.isOpen();
}
@Override
public int pairingPasswordPBKDF2IterationCount() {
return 10;
}
}
@@ -0,0 +1,83 @@
package im.status.keycard.desktop;
import im.status.keycard.globalplatform.Crypto;
import im.status.keycard.io.CardListener;
import org.hid4java.*;
import org.hid4java.event.HidServicesEvent;
public class LedgerUSBManager implements HidServicesListener {
static {
Crypto.addBouncyCastleProvider();
}
private static final int VID = 0x2c97;
private static final int[] PIDS = { 0x0001, 0x0004 };
private static final int SCAN_INTERVAL_MS = 500;
private static final int PAUSE_INTERVAL_MS = 5000;
private HidServices hidServices;
private CardListener listener;
public LedgerUSBManager(CardListener listener) {
this.listener = listener;
HidServicesSpecification hidServicesSpecification = new HidServicesSpecification();
hidServicesSpecification.setAutoShutdown(true);
hidServicesSpecification.setScanInterval(SCAN_INTERVAL_MS);
hidServicesSpecification.setPauseInterval(PAUSE_INTERVAL_MS);
hidServicesSpecification.setScanMode(ScanMode.SCAN_AT_FIXED_INTERVAL_WITH_PAUSE_AFTER_WRITE);
hidServices = HidManager.getHidServices(hidServicesSpecification);
hidServices.addHidServicesListener(this);
}
public void start() {
hidServices.start();
for (int pid : PIDS) {
HidDevice hidDevice = hidServices.getHidDevice(VID, pid, null);
if (hidDevice != null) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
break;
}
}
}
public void stop() {
hidServices.shutdown();
}
@Override
public void hidDeviceAttached(HidServicesEvent event) {
HidDevice hidDevice = event.getHidDevice();
if (isLedger(hidDevice)) {
listener.onConnected(new LedgerUSBChannel(hidDevice));
}
}
@Override
public void hidDeviceDetached(HidServicesEvent event) {
hidFailure(event);
}
@Override
public void hidFailure(HidServicesEvent event) {
if (isLedger(event.getHidDevice())) {
listener.onDisconnected();
}
}
private boolean isLedger(HidDevice hidDevice) {
for (int pid : PIDS) {
if (hidDevice.isVidPidSerial(VID, pid, null)) {
return true;
}
}
return false;
}
}
@@ -15,7 +15,7 @@ import java.io.IOException;
*/
public class PCSCCardChannel implements CardChannel {
static {
Crypto.addSpongyCastleProvider();
Crypto.addBouncyCastleProvider();
}
private javax.smartcardio.CardChannel cardChannel;
@@ -11,11 +11,20 @@ public class ApplicationInfo {
private short appVersion;
private byte freePairingSlots;
private byte[] keyUID;
private byte capabilities;
public static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
public static final byte TLV_PUB_KEY = (byte) 0x80;
public static final byte TLV_UID = (byte) 0x8F;
public static final byte TLV_KEY_UID = (byte) 0x8E;
public static final byte TLV_CAPABILITIES = (byte) 0x8D;
static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01;
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04;
static final byte CAPABILITY_NDEF = (byte) 0x08;
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF;
/**
* Constructs an object by parsing the TLV data.
@@ -32,6 +41,12 @@ public class ApplicationInfo {
if (topTag == TLV_PUB_KEY) {
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
initializedCard = false;
capabilities = CAPABILITY_CREDENTIALS_MANAGEMENT;
if (secureChannelPubKey.length > 0) {
capabilities |= CAPABILITY_SECURE_CHANNEL;
}
return;
}
@@ -41,6 +56,14 @@ public class ApplicationInfo {
appVersion = (short) tlv.readInt();
freePairingSlots = (byte) tlv.readInt();
keyUID = tlv.readPrimitive(TLV_KEY_UID);
if (tlv.readTag() != TinyBERTLV.END_OF_TLV) {
tlv.unreadLastTag();
capabilities = tlv.readPrimitive(TLV_CAPABILITIES)[0];
} else {
capabilities = CAPABILITIES_ALL;
}
initializedCard = true;
}
@@ -95,6 +118,14 @@ public class ApplicationInfo {
* @return the string representation of the application version
*/
public String getAppVersionString() {
return getAppVersionString(appVersion);
}
/**
* A formatted application version.
* @return the string representation of the application version
*/
static String getAppVersionString(short appVersion) {
return (appVersion >> 8) + "." + (appVersion & 0xff);
}
@@ -115,4 +146,49 @@ public class ApplicationInfo {
public byte[] getKeyUID() {
return keyUID;
}
/**
* Returns the capability descriptor for the device.
*
* @return the capability descriptor for the device.
*/
public byte getCapabilities() {
return capabilities;
}
/**
* Returns true if the device supports the Secure Channel capability.
*
* @return true or false
*/
public boolean hasSecureChannelCapability() {
return (capabilities & CAPABILITY_SECURE_CHANNEL) == CAPABILITY_SECURE_CHANNEL;
}
/**
* Returns true if the device supports the Key Management capability.
*
* @return true or false
*/
public boolean hasKeyManagementCapability() {
return (capabilities & CAPABILITY_KEY_MANAGEMENT) == CAPABILITY_KEY_MANAGEMENT;
}
/**
* Returns true if the device supports the Credentials Management capability.
*
* @return true or false
*/
public boolean hasCredentialsManagementCapability() {
return (capabilities & CAPABILITY_CREDENTIALS_MANAGEMENT) == CAPABILITY_CREDENTIALS_MANAGEMENT;
}
/**
* Returns true if the device supports the NDEF capability.
*
* @return true or false
*/
public boolean hasNDEFCapability() {
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
}
}
@@ -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;
@@ -67,13 +66,13 @@ public class BIP32KeyPair {
tlv.unreadLastTag();
privKey = tlv.readPrimitive(TLV_PRIV_KEY);
tag = tlv.readTag();
if (tag == TLV_CHAIN_CODE) {
tlv.unreadLastTag();
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
}
}
if (tag == TLV_CHAIN_CODE) {
tlv.unreadLastTag();
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
}
return new BIP32KeyPair(privKey, chainCode, pubKey);
}
@@ -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,96 +0,0 @@
package im.status.keycard.applet;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.CardChannel;
import java.io.IOException;
import java.security.SecureRandom;
/**
* Class helping with the card duplication process. Depending on the device's role, only some of the methods are relevant.
*
* WORK IN PROGRESS, DO NOT USE YET
*/
public class CardDuplicator {
private byte[] secret;
/**
* Creates a CardDuplicator object. Regardless of the role of the device, this object must be kept and used for the
* entire duplication session. It cannot be reused for multiple sessions.
*/
public CardDuplicator() {
secret = new byte[32];
SecureRandom random = new SecureRandom();
random.nextBytes(secret);
}
private KeycardCommandSet preamble(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.setPairing(pairing);
cmdSet.autoOpenSecureChannel();
cmdSet.verifyPIN(pin).checkOK();
return cmdSet;
}
/**
* Starts duplication session. Must be used on all cards taking part of in the duplication process.
*
* @param channel the card channel
* @param pairing the pairing info
* @param pin the card PIN
* @param deviceCount the number of devices which will be adding entropy for the key, including this one
*
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void startDuplication(CardChannel channel, Pairing pairing, String pin, int deviceCount) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
cmdSet.duplicateKeyStart(deviceCount, secret).checkOK();
}
/**
* Exports key. Must be used on the card designated as the source for the duplication.
*
* @param channel the card channel
* @param pairing the pairing info
* @param pin the card PIN
*
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] exportKey(CardChannel channel, Pairing pairing, String pin) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyExport().checkOK().getData();
}
/**
* Imports key. Must be used on all cards designated as the target for the duplication.
* @param channel the card channel
* @param pairing the pairing info
* @param pin the user PIN
* @param key the key to import
* @return the key UID
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public byte[] importKey(CardChannel channel, Pairing pairing, String pin, byte[] key) throws IOException, APDUException {
KeycardCommandSet cmdSet = preamble(channel, pairing, pin);
return cmdSet.duplicateKeyImport(key).checkOK().getData();
}
/**
* Adds entropy. Must be used on all cards taking part in the backup process. Each device taking part must use this
* exactly once, except for the device which started the backup.
*
* @param channel
* @throws IOException communication error
* @throws APDUException unexpected card response
*/
public void addEntropy(CardChannel channel) throws IOException, APDUException {
KeycardCommandSet cmdSet = new KeycardCommandSet(channel);
cmdSet.select().checkOK();
cmdSet.duplicateKeyAddEntropy(secret).checkOK();
}
}
@@ -0,0 +1,62 @@
package im.status.keycard.applet;
/**
* Parses the response from a SELECT command sent to the Cash applet.
*/
public class CashApplicationInfo {
public static final byte TLV_PUB_DATA = (byte) 0x82;
private byte[] pubKey;
private short appVersion;
private byte[] pubData;
/**
* Constructs an object by parsing the TLV data.
*
* @param tlvData the raw response data from the card
* @throws IllegalArgumentException the TLV does not follow the allowed format
*/
public CashApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE);
pubKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
appVersion = (short) tlv.readInt();
pubData = tlv.readPrimitive(TLV_PUB_DATA);
}
/**
* The public key of the wallet.
*
* @return the public key
*/
public byte[] getPubKey() {
return pubKey;
}
/**
* The application version, encoded as a short. The msb is the major revision number and the lsb is the minor one.
*
* @return the application version
*/
public short getAppVersion() {
return appVersion;
}
/**
* A formatted application version.
* @return the string representation of the application version
*/
public String getAppVersionString() {
return ApplicationInfo.getAppVersionString(appVersion);
}
/**
* The public data of the cash applet.
*
* @return the public key
*/
public byte[] getPubData() {
return pubData;
}
}
@@ -0,0 +1,80 @@
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 an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long
*
* @param challenge the data of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse identifyCard(byte[] challenge) throws IOException {
APDUCommand identifyCard = new APDUCommand(0x80, KeycardCommandSet.INS_IDENTIFY_CARD, 0, 0, challenge);
return apduChannel.send(identifyCard);
}
/**
* 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);
}
}
@@ -0,0 +1,141 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.util.Arrays;
public class Certificate extends RecoverableSignature {
public static final byte TLV_CERT = (byte) 0x8A;
private byte[] identPriv;
private byte[] identPub;
public Certificate(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) {
super(publicKey, compressed,r, s, recId);
}
public static KeyPair generateIdentKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256k1");
keyPairGenerator.initialize(spec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static Certificate createCertificate(KeyPair caPair, KeyPair identKeys) {
try {
byte[] pub = ((ECPublicKey) identKeys.getPublic()).getQ().getEncoded(true);
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
byte[] hash = md.digest(pub);
Signature signer = Signature.getInstance("NONEwithECDSA", "BC");
signer.initSign(caPair.getPrivate());
signer.update(hash);
byte[] sig = signer.sign();
TinyBERTLV tlv = new TinyBERTLV(sig);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
byte[] r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
byte[] s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
Certificate cert = new Certificate(((ECPublicKey)caPair.getPublic()).getQ().getEncoded(true), true, r, s, -1);
cert.calculateRecID(hash);
cert.identPriv = toUInt(((ECPrivateKey) identKeys.getPrivate()).getD().toByteArray());
cert.identPub = pub;
return cert;
} catch(IllegalArgumentException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static Certificate generateNewCertificate(KeyPair caPair) {
return createCertificate(caPair, generateIdentKeyPair());
}
public static Certificate fromTLV(byte[] certData) {
try {
byte[] pub = Arrays.copyOfRange(certData, 0, 33);
byte[] r = Arrays.copyOfRange(certData, 33, 65);
byte[] s = Arrays.copyOfRange(certData, 65, 97);
int recId = certData[97];
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
byte[] hash = md.digest(pub);
byte[] caPub = recoverFromSignature(recId, hash, r, s, true);
Certificate cert = new Certificate(caPub, true, r, s, recId);
cert.identPub = pub;
return cert;
} catch(IllegalArgumentException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public static byte[] verifyIdentity(byte[] hash, byte[] tlvData) {
try {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
byte[] certData = tlv.readPrimitive(TLV_CERT);
Certificate cert = fromTLV(certData);
byte[] signature = tlv.peekUnread();
Signature verifier = Signature.getInstance("NONEWithECDSA", "BC");
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(cert.identPub), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
verifier.initVerify(cardKey);
verifier.update(hash);
if (!verifier.verify(signature)) {
return null;
}
return cert.getPublicKey();
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?");
}
}
public byte[] toStoreData() {
if (identPriv == null) {
throw new IllegalStateException("The private key must be set.");
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
os.write(this.identPub);
os.write(this.getR());
os.write(this.getS());
os.write(this.getRecId());
os.write(this.identPriv);
} catch(IOException e) {
throw new RuntimeException(e);
}
return os.toByteArray();
}
}
@@ -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);
}
}
@@ -0,0 +1,46 @@
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 Ident applet.
*/
public class IdentCommandSet {
private final CardChannel apduChannel;
/**
* Creates a IdentCommandSet using the given APDU Channel
* @param apduChannel APDU channel
*/
public IdentCommandSet(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.IDENT_INSTANCE_AID);
return apduChannel.send(selectApplet);
}
/**
* Sends a STORE DATA APDU.
*
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse storeData(byte[] data) throws IOException {
APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_STORE_DATA, 0x00, 0x00, data);
return apduChannel.send(sign);
}
}
@@ -2,21 +2,45 @@ package im.status.keycard.applet;
import org.bouncycastle.util.encoders.Hex;
import java.util.Arrays;
public class Identifiers {
public static final byte[] PACKAGE_AID = Hex.decode("53746174757357616C6C6574");
public static final byte[] PACKAGE_AID = Hex.decode("A0000008040001");
public static final byte[] KEYCARD_AID = Hex.decode("53746174757357616C6C6574417070");
public static final byte[] KEYCARD_AID = Hex.decode("A000000804000101");
public static final int KEYCARD_DEFAULT_INSTANCE_IDX = 1;
public static final byte[] NDEF_AID = Hex.decode("53746174757357616C6C65744E4643");
public static final byte[] NDEF_AID = Hex.decode("A000000804000102");
public static final byte[] NDEF_INSTANCE_AID = Hex.decode("D2760000850101");
public static final byte[] CASH_AID = Hex.decode("A000000804000103");
public static final byte[] CASH_INSTANCE_AID = Hex.decode("A00000080400010301");
public static final byte[] IDENT_AID = Hex.decode("A000000804000104");
public static final byte[] IDENT_INSTANCE_AID = Hex.decode("A00000080400010401");
/**
* Gets the instance AID of the Keycard applet. Since multiple instances this is a method instead of a constant.
* Soon a method taking an additional instance index will be added.
* Gets the instance AID of the default instance of the Keycard applet.
*
* @return the instance AID of the Keycard applet
*/
public static byte[] getKeycardInstanceAID() {
return KEYCARD_AID;
return getKeycardInstanceAID(KEYCARD_DEFAULT_INSTANCE_IDX);
}
/**
* Gets the instance AID of the Keycard applet with the given index. Since multiple instances of the Keycard applet
* could be installed in parallel, this method allows selecting a specific instance. The index is between 01 and ff
*
* @return the instance AID of the Keycard applet
*/
public static byte[] getKeycardInstanceAID(int instanceIdx) {
if (instanceIdx < 0x01 || instanceIdx > 0xff) {
throw new IllegalArgumentException("The instance index must be between 1 and 255");
}
byte[] instanceAID = Arrays.copyOf(KEYCARD_AID, KEYCARD_AID.length + 1);
instanceAID[KEYCARD_AID.length] = (byte) instanceIdx;
return instanceAID;
}
}
@@ -1,9 +1,6 @@
package im.status.keycard.applet;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import im.status.keycard.io.*;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
@@ -23,6 +20,7 @@ public class KeycardCommandSet {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_SET_NDEF = (byte) 0xF3;
static final byte INS_IDENTIFY_CARD = (byte) 0x14;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
@@ -31,10 +29,15 @@ public class KeycardCommandSet {
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
static final byte INS_REMOVE_KEY = (byte) 0xD3;
static final byte INS_GENERATE_KEY = (byte) 0xD4;
static final byte INS_DUPLICATE_KEY = (byte) 0xD5;
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
static final byte INS_GET_DATA = (byte) 0xCA;
static final byte INS_STORE_DATA = (byte) 0xE2;
public static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
public static final byte CHANGE_PIN_P1_PUK = 0x01;
public static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02;
public static final byte GET_STATUS_P1_APPLICATION = 0x00;
public static final byte GET_STATUS_P1_KEY_PATH = 0x01;
@@ -52,6 +55,18 @@ public class KeycardCommandSet {
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
static final byte SIGN_P1_CURRENT_KEY = 0x00;
static final byte SIGN_P1_DERIVE = 0x01;
static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte SIGN_P1_PINLESS = 0x03;
public static final byte SIGN_P2_ECDSA = 0x00;
public static final byte SIGN_P2_BLS12_381 = 0x01;
public static final byte STORE_DATA_P1_PUBLIC = 0x00;
public static final byte STORE_DATA_P1_NDEF = 0x01;
public static final byte STORE_DATA_P1_CASH = 0x02;
public static final int GENERATE_MNEMONIC_12_WORDS = 0x04;
public static final int GENERATE_MNEMONIC_15_WORDS = 0x05;
public static final int GENERATE_MNEMONIC_18_WORDS = 0x06;
@@ -62,19 +77,39 @@ public class KeycardCommandSet {
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
public static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
public static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
public static final byte EXPORT_KEY_P2_EXTENDED_PUBLIC = 0x02;
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
private ApplicationInfo info;
/**
* Creates a KeycardCommandSet using the given APDU Channel
* @param apduChannel APDU channel
*/
public KeycardCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.secureChannel = new SecureChannelSession();
}
/**
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
* command has been sent using this command set.
*
* @return the application info object
*/
public ApplicationInfo getApplicationInfo() {
return info;
}
/**
* Set the SecureChannel object
* @param secureChannel secure channel
*/
protected void setSecureChannel(SecureChannelSession secureChannel) {
this.secureChannel = secureChannel;
}
@@ -95,19 +130,36 @@ public class KeycardCommandSet {
}
/**
* Selects the applet. The applet is assumed to have been installed with its default AID. The returned data is a
* public key which must be used to initialize the secure channel.
* Selects the default instance of the Keycard applet. The applet is assumed to have been installed with its default
* AID. The returned data is a public key which must be used to initialize the secure channel.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID());
return select(Identifiers.KEYCARD_DEFAULT_INSTANCE_IDX);
}
/**
* Selects a Keycard instance. The applet is assumed to have been installed with its default AID. The returned data is
* a public key which must be used to initialize the secure channel.
*
* @param instanceIdx the instance index
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select(int instanceIdx) throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID(instanceIdx));
APDUResponse resp = apduChannel.send(selectApplet);
if (resp.getSw() == 0x9000) {
this.secureChannel.generateSecret(extractPublicKeyFromSelect(resp.getData()));
this.secureChannel.reset();
info = new ApplicationInfo(resp.getData());
if (info.hasSecureChannelCapability()) {
this.secureChannel.generateSecret(info.getSecureChannelPubKey());
this.secureChannel.reset();
}
}
return resp;
@@ -117,8 +169,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);
}
@@ -126,8 +179,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);
@@ -144,7 +198,7 @@ public class KeycardCommandSet {
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", "BC");
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), 50000, 32 * 8);
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), apduChannel.pairingPasswordPBKDF2IterationCount(), 32 * 8);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
@@ -156,8 +210,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);
}
@@ -165,8 +220,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);
}
@@ -207,11 +263,26 @@ public class KeycardCommandSet {
/**
* Unpair all other clients.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
}
/**
* Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long
*
* @param challenge the data of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse identifyCard(byte[] challenge) throws IOException {
APDUCommand identifyCard = secureChannel.protectedCommand(0x80, INS_IDENTIFY_CARD, 0, 0, challenge);
return secureChannel.transmit(apduChannel, identifyCard);
}
/**
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
* class itself.
@@ -225,23 +296,11 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, getStatus);
}
/**
* Sends a SET NDEF APDU.
*
* @param ndef the data field of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setNDEF(byte[] ndef) throws IOException {
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
return secureChannel.transmit(apduChannel, setNDEF);
}
/**
/**
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pin the pin
* @param pin the PIN
* @return the raw card response
* @throws IOException communication error
*/
@@ -250,6 +309,40 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, verifyPIN);
}
/**
* Sends a CHANGE PIN APDU to change the user PIN.
*
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(String pin) throws IOException {
return changePIN(CHANGE_PIN_P1_USER_PIN, pin.getBytes());
}
/**
* Sends a CHANGE PIN APDU to change the PUK.
*
* @param puk the new PUK
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePUK(String puk) throws IOException {
return changePIN(CHANGE_PIN_P1_PUK, puk.getBytes());
}
/**
* Sends a CHANGE PIN APDU to change the pairing password. This does not break existing pairings, but new pairings
* will be made using the new password.
*
* @param pairingPassword the new pairing password
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePairingPassword(String pairingPassword) throws IOException {
return changePIN(CHANGE_PIN_P1_PAIRING_SECRET, pairingPasswordToSecret(pairingPassword));
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
@@ -411,65 +504,57 @@ public class KeycardCommandSet {
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 00, P2 to the entropy count and the data is the first entropy piece.
* This starts a duplication session. Requires an open Secure Channel and authenticated PIN.
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
*
* @param entropyCount the number of entropy pieces to expect, including the one in this APDU
* @param firstEntropy a random 32-byte number
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyStart(int entropyCount, byte[] firstEntropy) throws IOException {
APDUCommand duplicateKeyStart = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_START, entropyCount, firstEntropy);
return secureChannel.transmit(apduChannel, duplicateKeyStart);
public APDUResponse sign(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_CURRENT_KEY);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 01 and the data is the entropy. This adds entropy and does not require
* a Secure Channel or authenticated PIN.
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The key used to sign is given
* as a parameter.
*
* @param entropy a random 32-byte number
* @param hash the hash to sign
* @params path the path of the key to use
* @param makeCurrent ture if the key used to sign should become the current key, false otherwise
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyAddEntropy(byte[] entropy) throws IOException {
APDUCommand duplicateKeyAddEntropy = new APDUCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_ADD_ENTROPY, 0, secureChannel.oneShotEncrypt(entropy));
return apduChannel.send(duplicateKeyAddEntropy);
public APDUResponse signWithPath(byte[] hash, String path, boolean makeCurrent) throws IOException {
KeyPath keyPath = new KeyPath(path);
byte[] pathData = keyPath.getData();
byte[] data = Arrays.copyOf(hash, hash.length + pathData.length);
System.arraycopy(pathData, 0, data, hash.length, pathData.length);
return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE));
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 02. This exports the encrypted master key including chaining code.
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The pinless path will be used
* to sign. This command is the only variant of SIGN which can also be executed without a Secure Channel.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyExport() throws IOException {
APDUCommand duplicateKeyExport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_EXPORT, 0, new byte[0]);
return secureChannel.transmit(apduChannel, duplicateKeyExport);
public APDUResponse signPinless(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_PINLESS);
}
/**
* Sends a DUPLICATE KEY APDU. The P1 is set to 03. This imports an encrypted master key including chaining code. The
* response data contains the key UID of the imported key.
*
* @param key the key, exported from another card in the same duplication session.
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse duplicateKeyImport(byte[] key) throws IOException {
APDUCommand duplicateKeyImport = secureChannel.protectedCommand(0x80, INS_DUPLICATE_KEY, DUPLICATE_KEY_P1_IMPORT, 0, key);
return secureChannel.transmit(apduChannel, duplicateKeyImport);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
* a derivation path.
*
* @param p1 the p1 parameter
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, 0x00, 0x00, data);
public APDUResponse sign(byte[] data, int p1) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x00, data);
return secureChannel.transmit(apduChannel, sign);
}
@@ -510,6 +595,33 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The path must be absolute, that is starting from the master key.
* @param path the path. Must be an absolute path (i.e: starting from the master key)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(String path) throws IOException {
KeyPath keyPath = new KeyPath(path);
if (keyPath.getSource() != DERIVE_P1_SOURCE_MASTER) {
throw new IllegalArgumentException("Only absolute paths can be set as PINLESS path");
}
return setPinlessPath(keyPath.getData());
}
/**
* Sends an empty SET PINLESS PATH APDU, resetting it. After this command the card does not have a PINless path until
* a new one is set.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse resetPinlessPath() throws IOException {
return setPinlessPath(new byte[]{});
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
@@ -517,11 +629,15 @@ public class KeycardCommandSet {
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
public APDUResponse setPinlessPath(byte[] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
private byte poToP2(boolean publicOnly) {
return publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
}
/**
* Sends an EXPORT KEY APDU to export the current key.
*
@@ -530,9 +646,20 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportCurrentKey(boolean publicOnly) throws IOException {
return exportKey(EXPORT_KEY_P1_CURRENT, publicOnly, new byte[0]);
return exportCurrentKey(poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU to export the current key.
*
* @param p2 the p2 parameter
* @return the raw card reponse
* @throws IOException communication error
*/
public APDUResponse exportCurrentKey(byte p2) throws IOException {
return exportKey(EXPORT_KEY_P1_CURRENT, p2, new byte[0]);
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
@@ -543,10 +670,23 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(String keyPath, boolean makeCurrent, boolean publicOnly) throws IOException {
KeyPath path = new KeyPath(keyPath);
return exportKey(path.getData(), path.getSource(), makeCurrent, publicOnly);
return exportKey(keyPath, makeCurrent, poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param p2 the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(String keyPath, boolean makeCurrent, byte p2) throws IOException {
KeyPath path = new KeyPath(keyPath);
return exportKey(path.getData(), path.getSource(), makeCurrent, p2);
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
@@ -557,10 +697,23 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, boolean publicOnly) throws IOException {
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
return exportKey(p1, publicOnly, keyPath);
return exportKey(keyPath, source, makeCurrent, poToP2(publicOnly));
}
/**
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param p2 the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, byte p2) throws IOException {
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
return exportKey(p1, p2, keyPath);
}
/**
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
*
@@ -571,13 +724,74 @@ public class KeycardCommandSet {
* @throws IOException communication error
*/
public APDUResponse exportKey(int derivationOptions, boolean publicOnly, byte[] keypath) throws IOException {
byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, derivationOptions, p2, keypath);
return secureChannel.transmit(apduChannel, exportKey);
return exportKey(derivationOptions, poToP2(publicOnly), keypath);
}
/**
* Sends the INIT command to the card.
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
*
* @param derivationOptions the P1 parameter
* @param p2 the P2 parameter
* @param keypath the data parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(int derivationOptions, byte p2, byte[] keypath) throws IOException {
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, derivationOptions, p2, keypath);
return secureChannel.transmit(apduChannel, exportKey);
}
/**
* Sends a GET DATA APDU.
*
* @param dataType the type of data to be stored
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse getData(byte dataType) throws IOException {
APDUCommand getData = secureChannel.protectedCommand(0x80, INS_GET_DATA, dataType, 0, new byte[0]);
return secureChannel.transmit(apduChannel, getData);
}
/**
* Sends a STORE DATA APDU for NDEF.
*
* @param ndef the data field of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setNDEF(byte[] ndef) throws IOException {
if ((info.getAppVersion() >> 8) > 2) {
if ((ndef.length - 2) != ((ndef[0] << 8) | ndef[1])) {
byte[] tmp = new byte[ndef.length + 2];
tmp[0] = (byte) (ndef.length >> 8);
tmp[1] = (byte) (ndef.length & 0xff);
System.arraycopy(ndef, 0, tmp, 2, ndef.length);
ndef = tmp;
}
return storeData(ndef, STORE_DATA_P1_NDEF);
} else {
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
return secureChannel.transmit(apduChannel, setNDEF);
}
}
/**
* Sends a STORE DATA APDU.
*
* @param data the data field of the APDU
* @param dataType the type of data to be stored
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse storeData(byte[] data, byte dataType) throws IOException {
APDUCommand storeData = secureChannel.protectedCommand(0x80, INS_STORE_DATA, dataType, 0, data);
return secureChannel.transmit(apduChannel, storeData);
}
/**
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
*
* @param pin the PIN
* @param puk the PUK
@@ -586,9 +800,40 @@ 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, null, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param altPin the alternative 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 altPin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
return this.init(pin, altPin, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
}
/**
* Sends the INIT command to the card.
*
@@ -599,14 +844,47 @@ 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, null, 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 pin the alternative
* @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 altPin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException {
int baselen = pin.length() + puk.length() + sharedSecret.length;
int extlen;
if (altPin != null) {
extlen = 2 + altPin.length();
} else if ((pinRetries != 0) || (pukRetries != 0)) {
extlen = 2;
} else {
extlen = 0;
}
byte[] initData = Arrays.copyOf(pin.getBytes(), baselen + extlen);
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
if (extlen > 0) {
initData[baselen] = pinRetries;
initData[baselen + 1] = pukRetries;
if (extlen > 2) {
System.arraycopy(altPin.getBytes(), 0, initData, baselen + 2, altPin.length());
}
}
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
private byte[] extractPublicKeyFromSelect(byte[] select) {
return new ApplicationInfo(select).getSecureChannelPubKey();
}
}
@@ -0,0 +1,106 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.SortedSet;
import java.util.TreeSet;
public class Metadata {
private String cardName;
private SortedSet<Long> wallets;
public static Metadata fromData(byte[] data) {
int version = (data[0] & 0xe0) >> 5;
if (version != 1) {
throw new RuntimeException("Invalid version");
}
int namelen = (data[0] & 0x1f);
int off = 1;
String cardName = new String(data, off, namelen, Charset.forName("US-ASCII"));
off += namelen;
SortedSet<Long> set = new TreeSet<>();
while(off < data.length) {
int[] start = TinyBERTLV.readNum(data, off);
int[] count = TinyBERTLV.readNum(data, start[1]);
off = count[1];
long s = start[0] & 0xffffffffl;
buildRange(set, s, (s + count[0]));
}
return new Metadata(cardName, set);
}
private static void buildRange(SortedSet<Long> set, long start, long end) {
for (long i = start; i <= end; i++) {
set.add(i);
}
}
Metadata(String cardName, SortedSet<Long> wallets) {
this.cardName = cardName;
this.wallets = wallets;
}
public Metadata(String cardName) {
this(cardName, new TreeSet<>());
}
public String getCardName() {
return cardName;
}
public void setCardName(String cardName) {
if (cardName.length() > 20) {
throw new IllegalArgumentException("card name too long");
}
this.cardName = cardName;
}
public SortedSet<Long> getWallets() {
return wallets;
}
public void addWallet(long w) {
this.wallets.add(w);
}
public void removeWallet(long w) {
this.wallets.remove(w);
}
public byte[] toByteArray() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] name = this.cardName.getBytes(Charset.forName("US-ASCII"));
os.write(0x20 | name.length);
os.write(name, 0, name.length);
if (wallets.isEmpty()) {
return os.toByteArray();
}
long start = wallets.first();
int len = 0;
for (Long w : wallets.tailSet(start + 1)) {
if (w == (start + len + 1)) {
len++;
} else {
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
len = 0;
start = w;
}
}
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
return os.toByteArray();
}
}
@@ -3,10 +3,6 @@ package im.status.keycard.applet;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Scanner;
public class Mnemonic {
private final static int WORDLIST_SIZE = 2048;
@@ -41,28 +37,11 @@ public class Mnemonic {
}
/**
* Retrieves the official BIP39 english wordlist from GitHub.
* Returns the official BIP39 english wordlist as fetched from https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt on 23 Oct 2019.
*
* @throws IOException network error
*/
public void fetchBIP39EnglishWordlist() throws IOException {
URL remoteList = new URL("https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt");
Scanner scanner = new Scanner(remoteList.openStream());
ArrayList<String> list = new ArrayList<>();
while(scanner.hasNextLine()) {
list.add(scanner.nextLine());
}
scanner.close();
if (list.size() != WORDLIST_SIZE) {
throw new IllegalArgumentException("The list must contain exactly 2048 entries");
}
this.wordlist = new String[WORDLIST_SIZE];
list.toArray(this.wordlist);
public void fetchBIP39EnglishWordlist() {
this.wordlist = MnemonicEnglishDictionary.words;
}
/**
@@ -145,7 +124,7 @@ public class Mnemonic {
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
throw new RuntimeException("Is Bouncycastle correctly initialized?", e);
}
return key.getEncoded();
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ public class RecoverableSignature {
private int recId;
private byte[] r;
private byte[] s;
private boolean compressed;
public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
@@ -41,15 +42,28 @@ public class RecoverableSignature {
public RecoverableSignature(byte[] hash, byte[] tlvData) {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
this.publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
r = tlv.readPrimitive(TinyBERTLV.TLV_INT);
s = tlv.readPrimitive(TinyBERTLV.TLV_INT);
this.r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.compressed = false;
calculateRecID(hash);
}
public RecoverableSignature(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) {
this.publicKey = publicKey;
this.r = r;
this.s = s;
this.compressed = compressed;
this.recId = recId;
}
void calculateRecID(byte[] hash) {
recId = -1;
for (int i = 0; i < 4; i++) {
byte[] candidate = recoverFromSignature(i, new BigInteger(1, hash), new BigInteger(1, r), new BigInteger(1, s));
byte[] candidate = recoverFromSignature(i, hash, r, s, compressed);
if (Arrays.equals(candidate, publicKey)) {
recId = i;
@@ -62,6 +76,14 @@ public class RecoverableSignature {
}
}
static byte[] toUInt(byte[] signedInt) {
if (signedInt[0] == 0) {
return Arrays.copyOfRange(signedInt, 1, signedInt.length);
} else {
return signedInt;
}
}
/**
* The public key associated to this signature.
*
@@ -97,7 +119,24 @@ public class RecoverableSignature {
return s;
}
private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) {
/**
* The Ethereum address of the signing key
*
* @return ethereum address of the signing key
*/
public byte[] getEthereumAddress() {
return Ethereum.toEthereumAddress(publicKey);
}
static byte[] recoverFromSignature(int recId, byte[] hash, byte[] r, byte[] s, boolean compressed) {
BigInteger h = new BigInteger(1, hash);
BigInteger br = new BigInteger(1, r);
BigInteger bs = new BigInteger(1, s);
return recoverFromSignature(recId, h, br, bs, compressed);
}
static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s, boolean compressed) {
BigInteger n = CURVE.getN();
BigInteger i = BigInteger.valueOf((long) recId / 2);
BigInteger x = r.add(i.multiply(n));
@@ -118,7 +157,7 @@ public class RecoverableSignature {
BigInteger srInv = rInv.multiply(s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
return q.getEncoded(false);
return q.getEncoded(compressed);
}
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
@@ -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,16 @@ public class TinyBERTLV {
*/
public int readInt() throws IllegalArgumentException {
byte[] val = readPrimitive(TLV_INT);
return TinyBERTLV.readVal(val, 0, val.length);
}
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");
}
/**
* Returns all unread bytes in the TLV.
*
* @return all unread bytes
*/
byte[] peekUnread() {
return Arrays.copyOfRange(buffer, pos, buffer.length);
}
/**
@@ -104,13 +153,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 {
@@ -21,13 +21,13 @@ public class Crypto {
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
private static boolean spongyCastleLoaded = false;
private static boolean bouncyCastleLoaded = false;
public static void addSpongyCastleProvider() {
if (!spongyCastleLoaded) {
public static void addBouncyCastleProvider() {
if (!bouncyCastleLoaded) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
spongyCastleLoaded = true;
bouncyCastleLoaded = true;
}
}
@@ -61,7 +61,7 @@ public class Crypto {
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
} catch (NoSuchProviderException e) {
throw new RuntimeException("SpongyCastle not installed");
throw new RuntimeException("BouncyCastle not installed");
}
}
@@ -121,6 +121,21 @@ public class Crypto {
}
}
public static byte[] ecb3des(byte[] key, byte[] data) {
try {
Cipher cipher = Cipher.getInstance("DESede/ECB/NoPadding", "BC");
SecretKeySpec keyDes = new SecretKeySpec(resizeKey24(key), "DES");
cipher.init(Cipher.ENCRYPT_MODE, keyDes);
return cipher.doFinal(data);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Could not encrypt data", e);
}
}
public static byte[] kcv3des(byte[] key) {
return Arrays.copyOf(ecb3des(key, NullBytes8), 3);
}
/**
* Generates a 3DES MAC for SCP02 communication
*
@@ -24,6 +24,7 @@ public class GlobalPlatformCommandSet {
static final byte INS_DELETE = (byte) 0xE4;
static final byte INS_INSTALL = (byte) 0xE6;
static final byte INS_LOAD = (byte) 0xE8;
static final byte INS_PUT_KEY = (byte) 0xD8;
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
@@ -37,7 +38,10 @@ public class GlobalPlatformCommandSet {
private SCP02Keys cardKeys;
private Session session;
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
private final byte[] gpDefaultKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
private final SCP02Keys gpDefaultKeys = new SCP02Keys(gpDefaultKey, gpDefaultKey, gpDefaultKey);
private final byte[] developmentKey = Hex.decode("c212e073ff8b4bbfaff4de8ab655221f");
/**
* Constructs a new command set with the given CardChannel.
@@ -46,7 +50,26 @@ public class GlobalPlatformCommandSet {
*/
public GlobalPlatformCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.cardKeys = new SCP02Keys(testKey, testKey);
setCardKeys(developmentKey);
}
/**
* Sets the given key as all of ENC, MAC and DEK static keys, used to derive session keys.
* @param key the key
*/
public void setCardKeys(byte[] key) {
setCardKeys(key, key, key);
}
/**
* Sets the the ENC, MAC and DEK static keys, used to derive session keys.
*
* @param encKey the ENC key
* @param macKey the MAC key
* @param dekKey the DEK key
*/
public void setCardKeys(byte[] encKey, byte[] macKey, byte[] dekKey) {
this.cardKeys = new SCP02Keys(encKey, macKey, dekKey);;
}
/**
@@ -74,7 +97,12 @@ public class GlobalPlatformCommandSet {
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
APDUResponse resp = apduChannel.send(cmd);
if (resp.isOK()) {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
try {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
} catch(APDUException e) {
this.session = SecureChannel.verifyChallenge(hostChallenge, gpDefaultKeys, resp);
this.session.markAsUsingFallbackKeys();
}
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
}
@@ -103,18 +131,93 @@ public class GlobalPlatformCommandSet {
return this.secureChannel.send(cmd);
}
/**
* Opens an SCP02 secure channel with default keys.
* Convenience method for openSecureChannel with auto key ugprade.
*
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
* @throws IOException communication error
*/
public void openSecureChannel() throws APDUException, IOException {
openSecureChannel(true);
}
/**
* Opens an SCP02 secure channel. If with the current keys the card cryptogram cannot be verified, an attempt is made
* to use the default GlobalPlatform keys instead. This does not require additional commands to the card. In case
* the autoUpgradeKeys is set to true and the default GlobalPlatform keys were used, a PUT KEY command is sent to
* change the keys to the current ones.
*
* @param autoUpgradeKeys upgrade keys if default GP keys are loaded
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
* @throws IOException communication error
*/
public void openSecureChannel(boolean autoUpgradeKeys) throws APDUException, IOException {
SecureRandom random = new SecureRandom();
byte[] hostChallenge = new byte[8];
random.nextBytes(hostChallenge);
initializeUpdate(hostChallenge).checkOK();
externalAuthenticate(hostChallenge).checkOK();
if (this.session.usesFallbackKeys() && autoUpgradeKeys) {
this.putSCP02Keys(this.cardKeys.getEncKeyData(), this.cardKeys.getMacKeyData(), this.cardKeys.getDekKeyData(), 0, 1).checkOK();
}
}
/**
* Sends a PUT KEY APDU to load or replace SCP02 keys. The key is used for all 3 of ENC, MAC and DEK.
*
* @param key the key to load
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
* @param newKvn the KVN of the new keyset
* @return
* @throws IOException
*/
public APDUResponse putSCP02Keys(byte[] key, int oldKvn, int newKvn) throws IOException {
return putSCP02Keys(key, key, key, oldKvn, newKvn);
}
/**
* Sends a PUT KEY APDU to load or replace SCP02 keys. The keys are assumed to be 3DES keys
*
* @param encKey the ENC key to load
* @param macKey the MAC key to load
* @param dekKey the DEK key to load
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
* @param newKvn the KVN of the new keyset
* @return
* @throws IOException
*/
public APDUResponse putSCP02Keys(byte[] encKey, byte[] macKey, byte[] dekKey, int oldKvn, int newKvn) throws IOException {
if (encKey.length != 16 || macKey.length != 16 || dekKey.length != 16){
throw new IllegalArgumentException("All keys must be 16-byte 3DES keys");
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(newKvn);
writeSCP02Key(bos, encKey);
writeSCP02Key(bos, macKey);
writeSCP02Key(bos, dekKey);
APDUCommand cmd = new APDUCommand(0x84, INS_PUT_KEY, oldKvn, 0x81, bos.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* writes an encrypted key for the PUT KEY command
* @param bos the output stream to write to
* @param key the key to encrypt and write
* @throws IOException if the ByteArrayOutputStream throws it (never)
*/
private void writeSCP02Key(ByteArrayOutputStream bos, byte[] key) throws IOException {
byte[] encrypted = Crypto.ecb3des(session.getKeys().getDekKeyData(), key);
byte[] kcv = Crypto.kcv3des(key);
bos.write(0x80);
bos.write(encrypted.length);
bos.write(encrypted);
bos.write(kcv.length);
bos.write(kcv);
}
/**
@@ -127,6 +230,16 @@ public class GlobalPlatformCommandSet {
return delete(Identifiers.getKeycardInstanceAID());
}
/**
* Deletes the Keycard Cash applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteCashInstance() throws IOException {
return delete(Identifiers.CASH_INSTANCE_AID);
}
/**
* Deletes the NDEF applet instance.
*
@@ -137,6 +250,16 @@ public class GlobalPlatformCommandSet {
return delete(Identifiers.NDEF_INSTANCE_AID);
}
/**
* Deletes the Ident applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteIdentInstance() throws IOException {
return delete(Identifiers.IDENT_INSTANCE_AID);
}
/**
* Deletes the Keycard package.
*
@@ -155,9 +278,7 @@ public class GlobalPlatformCommandSet {
* @throws IOException communication error
*/
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);
deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
delete(Identifiers.PACKAGE_AID, (byte) 0x80).checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
}
/**
@@ -168,15 +289,27 @@ public class GlobalPlatformCommandSet {
* @throws IOException communication error.
*/
public APDUResponse delete(byte[] aid) throws IOException {
return delete(aid, (byte) 0);
}
/**
* Sends a DELETE APDU with the given AID
* @param aid the AID to the delete
* @param p2 the P2 value
* @return the raw card response
*
* @throws IOException communication error.
*/
public APDUResponse delete(byte[] aid, byte p2) throws IOException {
byte[] data = new byte[aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) aid.length;
System.arraycopy(aid, 0, data, 2, aid.length);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, 0, data);
APDUCommand cmd = new APDUCommand(0x80, INS_DELETE, 0, p2, data);
return this.secureChannel.send(cmd);
}
}
/**
* Loads the Keycard package.
@@ -313,4 +446,35 @@ 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]);
}
/**
* Installs the Ident applet.
*
* @return the card response
* @throws IOException communication error.
*/
public APDUResponse installIdentApplet() throws IOException {
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.IDENT_AID, Identifiers.IDENT_INSTANCE_AID, new byte[0]);
}
}
@@ -6,16 +6,19 @@ package im.status.keycard.globalplatform;
public class SCP02Keys {
public byte[] encKeyData;
public byte[] macKeyData;
public byte[] dekKeyData;
/**
* Constructor. Takes the ENC and MAC keys.
*
* @param encKeyData encryption key
* @param macKeyData mac key
* @param dekKeyData data encryption key
*/
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
public SCP02Keys(byte[] encKeyData, byte[] macKeyData, byte[] dekKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
this.dekKeyData = dekKeyData;
}
/**
@@ -34,4 +37,13 @@ public class SCP02Keys {
public byte[] getMacKeyData() {
return macKeyData;
}
/**
* The DEK key
*
* @return the DEK key
*/
public byte[] getDekKeyData() {
return dekKeyData;
}
}
@@ -8,7 +8,7 @@ import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
/**
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the scure channel.
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the secure channel.
*/
public class SecureChannel {
private CardChannel channel;
@@ -77,8 +77,9 @@ public class SecureChannel {
byte[] sessionEncKey = Crypto.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
byte[] sessionDekKey = Crypto.deriveSCP02SessionKey(cardKeys.getDekKeyData(), seq, DERIVATION_PURPOSE_DEK);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey, sessionDekKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
@@ -6,6 +6,7 @@ package im.status.keycard.globalplatform;
public class Session {
private SCP02Keys keys;
private byte[] cardChallenge;
private boolean fallbackKeys;
/**
* Constructs the SCP02 session.
@@ -16,6 +17,7 @@ public class Session {
public Session(SCP02Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
this.fallbackKeys = false;
}
/**
@@ -33,4 +35,21 @@ public class Session {
public byte[] getCardChallenge() {
return cardChallenge;
}
/**
* Marks this session as using a fallback keyset.
*/
public void markAsUsingFallbackKeys() {
fallbackKeys = true;
}
/**
* True if a fallback keyset is being used.
*
* @return true or false
*/
public boolean usesFallbackKeys() {
return fallbackKeys;
}
}
@@ -10,6 +10,7 @@ public class APDUResponse {
public static final int SW_CARD_LOCKED = 0x6283;
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
public static final int SW_WRONG_PIN_MASK = 0x63C0;
private byte[] apdu;
private byte[] data;
@@ -87,6 +88,49 @@ 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)
*
* @throws WrongPINException wrong PIN
* @throws APDUException unexpected response
*/
public APDUResponse checkAuthOK() throws WrongPINException, APDUException {
if ((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
throw new WrongPINException(sw2 & 0x0F);
} else {
return checkOK();
}
}
/**
* Returns the data field of this APDU.
*
@@ -20,4 +20,16 @@ public interface CardChannel {
* @return true if connected, false otherwise
*/
boolean isConnected();
/**
* Returns the iteration count for deriving the pairing key from the pairing password. The default is 50000 and is
* should only be changed for devices where the PBKDF2 is calculated on-board and the resource do not permit a
* high iteration count. If a lower count is used other security mechanism should be used to prevent brute force
* attacks.
*
* @return the iteration count
*/
default int pairingPasswordPBKDF2IterationCount() {
return 50000;
}
}
@@ -0,0 +1,153 @@
package im.status.keycard.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class LedgerUtil {
private static final int LEDGER_DEFAULT_CHANNEL = 1;
private static final int TAG_APDU = 0x05;
private LedgerUtil() {}
public interface Callback {
void write(byte[] chunk) throws IOException;
void read(byte[] chunk) throws IOException;
}
public static APDUResponse send(APDUCommand cmd, int segmentSize, boolean channelInfo, LedgerUtil.Callback cb) throws IOException {
int offset = 0;
byte[] command = LedgerUtil.wrapCommandAPDU(cmd.serialize(), segmentSize, channelInfo);
byte[] chunk = new byte[segmentSize];
while(offset != command.length) {
System.arraycopy(command, offset, chunk, 0, segmentSize);
cb.write(chunk);
offset += segmentSize;
}
ByteArrayOutputStream response = new ByteArrayOutputStream();
byte[] responseData = null;
while ((responseData = LedgerUtil.unwrapResponseAPDU(response.toByteArray(), segmentSize, channelInfo)) == null) {
cb.read(chunk);
response.write(chunk, 0, segmentSize);
}
return new APDUResponse(responseData);
}
private static byte[] unwrapResponseAPDU(byte[] data, int segmentSize, boolean channelInfo) throws IOException {
if ((data == null) || (data.length < 7 + 5)) {
return null;
}
int sequenceIdx = 0;
int offset = checkResponseHeader(data, 0, sequenceIdx, channelInfo);
int responseLength = ((data[offset++] & 0xff) << 8);
responseLength |= (data[offset++] & 0xff);
if (data.length < 7 + responseLength) {
return null;
}
ByteArrayOutputStream response = new ByteArrayOutputStream();
int headerSize = channelInfo ? 5 : 3;
int initialHeaderSize = headerSize + 2;
int blockSize = (responseLength > segmentSize - initialHeaderSize ? segmentSize - initialHeaderSize : responseLength);
response.write(data, offset, blockSize);
offset += blockSize;
while (response.size() != responseLength) {
sequenceIdx++;
if (offset == data.length) {
return null;
}
offset = checkResponseHeader(data, offset, sequenceIdx, channelInfo);
blockSize = (responseLength - response.size() > segmentSize - headerSize ? segmentSize - headerSize : responseLength - response.size());
if (blockSize > data.length - offset) {
return null;
}
response.write(data, offset, blockSize);
offset += blockSize;
}
return response.toByteArray();
}
private static int checkResponseHeader(byte[] data, int offset, int sequenceIdx, boolean channelInfo) throws IOException {
if (channelInfo) {
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL >> 8)) {
throw new IOException("Invalid channel");
}
if (data[offset++] != (LEDGER_DEFAULT_CHANNEL & 0xff)) {
throw new IOException("Invalid channel");
}
}
if (data[offset++] != TAG_APDU) {
throw new IOException("Invalid tag");
}
if (data[offset++] != (sequenceIdx >> 8)) {
throw new IOException("Invalid sequence");
}
if (data[offset++] != (sequenceIdx & 0xff)) {
throw new IOException("Invalid sequence");
}
return offset;
}
private static byte[] wrapCommandAPDU(byte[] command, int segmentSize, boolean channelInfo) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
int headerSize = channelInfo ? 5 : 3;
int initialHeaderSize = headerSize + 2;
int sequenceIdx = 0;
int offset = 0;
writeCommandHeader(output, sequenceIdx, channelInfo);
sequenceIdx++;
output.write(command.length >> 8);
output.write(command.length);
int blockSize = (command.length > (segmentSize - initialHeaderSize) ? (segmentSize - initialHeaderSize) : command.length);
output.write(command, offset, blockSize);
offset += blockSize;
while (offset != command.length) {
writeCommandHeader(output, sequenceIdx, channelInfo);
sequenceIdx++;
blockSize = ((command.length - offset) > (segmentSize - headerSize) ? (segmentSize - headerSize) : (command.length - offset));
output.write(command, offset, blockSize);
offset += blockSize;
}
if ((output.size() % segmentSize) != 0) {
byte[] padding = new byte[segmentSize - (output.size() % segmentSize)];
output.write(padding, 0, padding.length);
}
return output.toByteArray();
}
private static void writeCommandHeader(ByteArrayOutputStream output, int sequenceIdx, boolean channelInfo) {
if (channelInfo) {
output.write(LEDGER_DEFAULT_CHANNEL >> 8);
output.write(LEDGER_DEFAULT_CHANNEL);
}
output.write(TAG_APDU);
output.write(sequenceIdx >> 8);
output.write(sequenceIdx);
}
}
@@ -0,0 +1,27 @@
package im.status.keycard.io;
/**
* Exception thrown when checking PIN/PUK
*/
public class WrongPINException extends APDUException {
private int retryAttempts;
/**
* Construct an exception with the given number of retry attempts.
*
* @param retryAttempts the number of retry attempts
*/
public WrongPINException(int retryAttempts) {
super("Wrong PIN");
this.retryAttempts = retryAttempts;
}
/**
* Returns the number of available retry attempts.
*
* @return the number of retry attempts
*/
public int getRetryAttempts() {
return retryAttempts;
}
}