Compare commits

...
85 Commits
Author SHA1 Message Date
Michele Balistreri 0d27ac445c treat malformed response as ioexception 2023-02-22 12:16:44 +01:00
Michele Balistreri ccb353ca82 handle securityexceptions 2023-02-06 13:01:59 +01:00
Michele Balistreri 78c6dfb6d6 support raw signature format 2022-12-09 12:31:56 +01:00
Michele Balistreri 15a61e16e7 Add init with alt PIN (#29)
* init with alt pin

* chain code in pubkeys
2022-11-21 08:43:55 +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
Bitgamma b08b1dc7ce Unified sdk (#12)
* unifying Android and Desktop SDK

* implement desktop SDK adapter

* updating declarations

* change include syntax

* following jitpack.io guide for Android libraries

* add install task to all artefacts, add javadoc generation

* fixing javadoc

* use explicit provider "SC" instead of relying on order

* move to BouncyCastle for desktop compatibility

* improve documentation
2018-12-11 11:50:53 +03:00
Bitgamma 07fc087cb3 [WIP] rename hardwallet to keycard (#11)
* rename hardwallet to keycard

* rename hardwallet to keycard

* rename hardwallet to keycard

* change constants

* add convenience GlobalPlatform methods

* add javadoc to all classes
2018-12-07 16:44:02 +03:00
Michele Balistreri c749e5a744 update EXPORT KEY command 2018-11-30 11:11:38 +03:00
Michele Balistreri 02fee8edd2 update EXPORT KEY command 2018-11-30 10:43:32 +03:00
Michele Balistreri c31df742dd filter idea folder 2018-11-27 14:35:44 +03:00
Bitgamma 3c759152e3 Delete codeStyleConfig.xml 2018-11-27 14:33:39 +03:00
Andrea Franz 735ccd80d9 Merge pull request #10 from status-im/feature/globalplatform-secure-channel
add installation commands
2018-11-27 12:21:45 +01:00
Andrea Franz 439237a75a add blocksCount method to Load class 2018-11-27 12:19:21 +01:00
Andrea Franz 5a2204bbcb add checkSW method 2018-11-27 12:02:52 +01:00
Andrea Franz 918aec72ac add installation commands 2018-11-27 12:02:52 +01:00
Michele Balistreri 145fe4b554 add to demo 2018-11-26 20:05:58 +03:00
Michele Balistreri 1575dc9d86 add BIP32KeyPair 2018-11-26 19:32:27 +03:00
Michele Balistreri ae65a94705 add missing import method 2018-11-26 11:45:54 +03:00
Andrea Franz a9726f3c50 Merge pull request #9 from status-im/feature/globalplatform-secure-channel
implement globalplatform secure channel commands
2018-11-22 17:20:54 +01:00
Michele Balistreri a85e1de2e6 add duplicate key 2018-11-22 17:59:20 +03:00
Michele Balistreri 3cad21254c add static method in Mnemonic to accept external phrase 2018-11-22 15:48:06 +03:00
Andrea Franz e781d5551f remove duplicated generatePairingKey 2018-11-22 13:09:11 +01:00
Andrea Franz 1ae29cd1cd rename Keys to SCP02Keys 2018-11-22 13:07:43 +01:00
Andrea Franz 346ab46187 rename APDUWrapper to SCP02Wrapper 2018-11-22 13:01:09 +01:00
Andrea Franz 41f2473f64 rename smartcard pkg to globalplatform 2018-11-22 12:57:19 +01:00
Andrea Franz 4399be35a4 rename deriveKey to deriveSCP02SessionKey 2018-11-22 12:55:58 +01:00
Andrea Franz ed7630bada improve appendDESPadding from the imported library 2018-11-22 12:48:03 +01:00
Michele Balistreri 03b0a56a09 fixed 2018-11-22 13:32:30 +03:00
Michele Balistreri a66f921ee8 add Mnemonic class 2018-11-22 11:39:49 +03:00
Andrea Franz 801accdde3 implement globalplatform secure channel commands 2018-11-21 18:14:01 +01:00
Michele Balistreri 002ade1c74 add conversion from keypath data to string 2018-11-16 11:12:33 +03:00
Andrea Franz f646e5d8ee Card listener (#8)
* update listener interface to have onDisconnected callback

* allow custom loop time

* check that cardListener is not null before invoking it
2018-11-15 21:25:08 +03:00
Michele Balistreri 931b6608d5 Closes #6, #7 2018-11-15 20:16:18 +03:00
Michele Balistreri 70b50c8989 Closes #5 2018-11-14 18:27:39 +03:00
Michele Balistreri 8b4e66e50b Add higher-level classes and improve the demo 2018-11-14 17:52:59 +03:00
Michele Balistreri f4fd086467 Closes #3 2018-11-14 11:41:00 +03:00
Bitgamma eb9f3f1350 [WIP] Update to version 2 (#2)
* v2

* add SET NDEF

* make a few constants public

* improve secure channel handling

* improve secure channel handling

* improve secure channel handling
2018-11-13 16:33:44 +03:00
86 changed files with 8538 additions and 1022 deletions
+4 -3
View File
@@ -1,10 +1,11 @@
*.iml
.gradle
.vscode
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.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.
+28
View File
@@ -0,0 +1,28 @@
# Keycard Java SDK for Android and Desktop
This SDK simplifies integration with the [Status Keycard](https://github.com/status-im/status-keycard) in Android
and Desktop applications. In this SDK you find both the classes needed for generic communication with SmartCards as well
as classes specifically addressing the Keycard.
To get started, check the file ```demo-android/src/main/java/im/status/keycard/app/MainActivity.java``` which a simple
demo application showing how the SDK works and what you can do with it.
## Usage
You can import the SDK in your Gradle or Maven project using [Jitpack.io](https://jitpack.io).
### On Android
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:android:3.0.2'
}
```
### on the desktop
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:3.0.2'
}
```
+8
View File
@@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
+59
View File
@@ -0,0 +1,59 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
group = 'com.github.status-im'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 304
versionName "3.0.4"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
}
dependencies {
api project(':lib')
}
// build a jar with source files
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
failOnError false
source = android.sourceSets.main.java.sourceFiles
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += configurations.compile
}
// build a jar with javadoc
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
android.libraryVariants.all { variant ->
def name = variant.buildType.name
def task = project.tasks.create "jar${name.capitalize()}", Jar
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
artifacts.add('archives', task);
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.status.keycard">
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<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;
}
}
@@ -0,0 +1,48 @@
package im.status.keycard.android;
import android.nfc.tech.IsoDep;
import android.util.Log;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import java.io.IOException;
/**
* Implementation of the CardChannel interface using the Android NFC API.
*/
public class NFCCardChannel implements CardChannel {
private static final String TAG = "CardChannel";
private IsoDep isoDep;
public NFCCardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
try {
byte[] resp = this.isoDep.transceive(apdu);
APDUResponse response = new APDUResponse(resp);
Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
return response;
} catch(SecurityException e) {
throw new IOException("Tag disconnected", e);
} catch(IllegalArgumentException e) {
throw new IOException("Malformed card response", e);
}
}
@Override
public boolean isConnected() {
try {
return this.isoDep.isConnected();
} catch(SecurityException e) {
return false;
}
}
}
@@ -0,0 +1,125 @@
package im.status.keycard.android;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.SystemClock;
import android.util.Log;
import im.status.keycard.globalplatform.Crypto;
import im.status.keycard.io.CardListener;
import java.io.IOException;
/**
* Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
* a runloop which monitors the connection and from which CardListener callbacks are called.
*/
public class NFCCardManager extends Thread implements NfcAdapter.ReaderCallback {
private static final String TAG = "NFCCardManager";
private static final int DEFAULT_LOOP_SLEEP_MS = 50;
private IsoDep isoDep;
private boolean isRunning;
private CardListener cardListener;
private int loopSleepMS;
static {
Crypto.addBouncyCastleProvider();
}
/**
* Constructs an NFC Card Manager with default delay between loop iterations.
*/
public NFCCardManager() {
this(DEFAULT_LOOP_SLEEP_MS);
}
/**
* Constructs an NFC Card Manager with the given delay between loop iterations.
*
* @param loopSleepMS time to sleep between loops
*/
public NFCCardManager(int loopSleepMS) {
this.loopSleepMS = loopSleepMS;
}
/**
* True if connected, false otherwise.
* @return if connected, false otherwise
*/
public boolean isConnected() {
try {
return isoDep != null && isoDep.isConnected();
} catch (SecurityException e) {
return false;
}
}
@Override
public void onTagDiscovered(Tag tag) {
isoDep = IsoDep.get(tag);
try {
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException | SecurityException e) {
Log.e(TAG, "error connecting to tag");
}
}
/**
* Runloop. Do NOT invoke directly. Use start() instead.
*/
public void run() {
boolean connected = isConnected();
while (true) {
boolean newConnected = isConnected();
if (newConnected != connected) {
connected = newConnected;
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
if (connected && !isRunning) {
onCardConnected();
} else {
onCardDisconnected();
}
}
SystemClock.sleep(loopSleepMS);
}
}
/**
* Reacts on card connected by calling the callback of the registered listener.
*/
private void onCardConnected() {
isRunning = true;
if (cardListener != null) {
cardListener.onConnected(new NFCCardChannel(isoDep));
}
isRunning = false;
}
/**
* Reacts on card disconnected by calling the callback of the registered listener.
*/
private void onCardDisconnected() {
isRunning = false;
isoDep = null;
if (cardListener != null) {
cardListener.onDisconnected();
}
}
/**
* Sets the card listener.
*
* @param listener the new listener
*/
public void setCardListener(CardListener listener) {
cardListener = listener;
}
}
+5 -4
View File
@@ -8,10 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
}
}
@@ -25,3 +22,7 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}
subprojects {
tasks.withType(Javadoc).all { enabled = false }
}
@@ -3,11 +3,11 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "im.status.hardwallet_lite_android.demo"
applicationId "im.status.keycard.demo"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
versionCode 300
versionName "3.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -16,13 +16,23 @@ 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 'org.bouncycastle:bcprov-jdk15on:1.60'
implementation project(':lib')
implementation project(':android')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
+5
View File
@@ -0,0 +1,5 @@
<lint>
<issue id="InvalidPackage">
<ignore path="**/bcprov-jdk15on-*.jar"/>
</issue>
</lint>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.status.hardwallet_lite_android.demo">
package="im.status.keycard.demo">
<application
android:allowBackup="true"
@@ -9,7 +9,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="im.status.hardwallet_lite_android.app.MainActivity">
<activity android:name="im.status.keycard.app.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -0,0 +1,197 @@
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.bouncycastle.util.encoders.Hex;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private NfcAdapter nfcAdapter;
private NFCCardManager cardManager;
//private LedgerBLEManager cardManager;
//private boolean connected;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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) {
try {
// Applet-specific code
KeycardCommandSet cmdSet = new KeycardCommandSet(cardChannel);
Log.i(TAG, "Applet selection successful");
// First thing to do is selecting the applet on the card.
ApplicationInfo info = new ApplicationInfo(cmdSet.select().checkOK().getData());
// If the card is not initialized, the INIT apdu must be sent. The actual PIN, PUK and pairing password values
// can be either generated or chosen by the user. Using fixed values is highly discouraged.
if (!info.isInitializedCard()) {
Log.i(TAG, "Initializing card with test secrets");
cmdSet.init("000000", "123456789012", "KeycardTest").checkOK();
info = new ApplicationInfo(cmdSet.select().checkOK().getData());
}
Log.i(TAG, "Instance UID: " + Hex.toHexString(info.getInstanceUID()));
Log.i(TAG, "Secure channel public key: " + Hex.toHexString(info.getSecureChannelPubKey()));
Log.i(TAG, "Application version: " + info.getAppVersionString());
Log.i(TAG, "Free pairing slots: " + info.getFreePairingSlots());
if (info.hasMasterKey()) {
Log.i(TAG, "Key UID: " + Hex.toHexString(info.getKeyUID()));
} 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());
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()));
// Opening a Secure Channel is needed for all other applet commands
cmdSet.autoOpenSecureChannel();
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());
Log.i(TAG, "PIN retry counter: " + status.getPINRetryCount());
Log.i(TAG, "PUK retry counter: " + status.getPUKRetryCount());
Log.i(TAG, "Has master key: " + status.hasMasterKey());
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();
Log.i(TAG, "Generated mnemonic phrase: " + mnemonic.toMnemonicPhrase());
Log.i(TAG, "Binary seed: " + Hex.toHexString(mnemonic.toBinarySeed()));
}
if (info.hasCredentialsManagementCapability()) {
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkAuthOK();
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() && info.hasKeyManagementCapability()) {
cmdSet.generateKey();
//cmdSet.loadKey(mnemonic.toBIP32KeyPair());
}
// Get the current key path using GET STATUS
KeyPath currentPath = new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData());
Log.i(TAG, "Current key path: " + currentPath);
if (!currentPath.toString().equals("m/44'/0'/0'/0/0")) {
// Key derivation is needed to select the desired key. The derived key remains current until a new derive
// command is sent (it is not lost on power loss).
cmdSet.deriveKey("m/44'/0'/0'/0/0").checkOK();
Log.i(TAG, "Derived m/44'/0'/0'/0/0");
}
// We retrieve the wallet public key
BIP32KeyPair walletPublicKey = BIP32KeyPair.fromTLV(cmdSet.exportCurrentKey(true).checkOK().getData());
Log.i(TAG, "Wallet public key: " + Hex.toHexString(walletPublicKey.getPublicKey()));
Log.i(TAG, "Wallet address: " + Hex.toHexString(walletPublicKey.toEthereumAddress()));
byte[] hash = "thiscouldbeahashintheorysoitisok".getBytes();
RecoverableSignature signature = new RecoverableSignature(hash, cmdSet.sign(hash).checkOK().getData());
Log.i(TAG, "Signed hash: " + Hex.toHexString(hash));
Log.i(TAG, "Recovery ID: " + signature.getRecId());
Log.i(TAG, "R: " + Hex.toHexString(signature.getR()));
Log.i(TAG, "S: " + Hex.toHexString(signature.getS()));
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());
}
}
@Override
public void onDisconnected() {
Log.i(TAG, "Card disconnected.");
}
});
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
public void onResume() {
super.onResume();
if (nfcAdapter != null) {
nfcAdapter.enableReaderMode(this, this.cardManager, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null);
}
}
@Override
public void onPause() {
super.onPause();
if (nfcAdapter != null) {
nfcAdapter.disableReaderMode(this);
}
}
}
@@ -5,7 +5,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".app.MainActivity">
tools:context="im.status.keycard.app.MainActivity">
<TextView
android:layout_width="wrap_content"

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">KeycardAndroid</string>
</resources>
@@ -1,95 +0,0 @@
package im.status.hardwallet_lite_android.app;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import im.status.hardwallet_lite_android.demo.R;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.hardwallet_lite_android.io.CardManager;
import im.status.hardwallet_lite_android.io.OnCardConnectedListener;
import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private NfcAdapter nfcAdapter;
private CardManager cardManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
cardManager = new CardManager();
cardManager.setOnCardConnectedListener(new OnCardConnectedListener() {
@Override
public void onConnected(CardChannel cardChannel) {
try {
Log.i(TAG, "onCardConnected()");
// Applet-specific code
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(cardChannel);
// First thing to do is selecting the applet on the card.
cmdSet.select().checkOK();
Log.i(TAG, "Applet is installed on the connected card.");
// In real projects, the pairing key should be saved and used for all new sessions.
cmdSet.autoPair("WalletAppletTest");
Log.i(TAG, "Pairing with card is done.");
// Opening a Secure Channel is needed for all other applet commands
cmdSet.autoOpenSecureChannel();
Log.i(TAG, "Secure channel opened.");
// We send a GET STATUS command, which does not require PIN authentication
APDUResponse resp = cmdSet.getStatus(WalletAppletCommandSet.GET_STATUS_P1_APPLICATION).checkOK();
Log.i(TAG, "Got status (response length=" + resp.getData().length + ")." );
// PIN authentication allows execution of privileged commands
cmdSet.verifyPIN("000000").checkOK();
Log.i(TAG, "Pin Verified.");
// 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());
}
}
});
cardManager.start();
}
@Override
public void onResume() {
super.onResume();
if (nfcAdapter != null) {
nfcAdapter.enableReaderMode(this, this.cardManager, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null);
}
}
@Override
public void onPause() {
super.onPause();
if (nfcAdapter != null) {
nfcAdapter.disableReaderMode(this);
}
}
}
-3
View File
@@ -1,3 +0,0 @@
<resources>
<string name="app_name">HardwalletLiteAndroid</string>
</resources>
+8
View File
@@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
.externalNativeBuild
+36
View File
@@ -0,0 +1,36 @@
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) {
classifier = 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
@@ -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;
}
}
@@ -0,0 +1,50 @@
package im.status.keycard.desktop;
import im.status.keycard.globalplatform.Crypto;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import java.io.IOException;
/**
* Implementation of a CardChannel using the Java Smartcard I/O API,
*/
public class PCSCCardChannel implements CardChannel {
static {
Crypto.addBouncyCastleProvider();
}
private javax.smartcardio.CardChannel cardChannel;
/**
* Constructor. Wraps a Java Smartcard I/O CardChannel.
* @param cardChannel the card channel to wrap.
*/
public PCSCCardChannel(javax.smartcardio.CardChannel cardChannel) {
this.cardChannel = cardChannel;
}
@Override
public APDUResponse send(APDUCommand cmd) throws IOException {
CommandAPDU capdu = new CommandAPDU(cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData(), cmd.getNeedsLE() ? 0x100 : 0x00);
ResponseAPDU rapdu;
try {
rapdu = cardChannel.transmit(capdu);
} catch (CardException e) {
throw new IOException(e);
}
return new APDUResponse(rapdu.getBytes());
}
@Override
public boolean isConnected() {
return true;
}
}
+18 -23
View File
@@ -1,26 +1,21 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.source
classifier = 'sources'
}
artifacts {
archives androidSourcesJar
}
}
apply plugin: 'java'
apply plugin: 'maven'
dependencies {
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'
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.status.hardwallet_lite_android">
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
</manifest>
@@ -1,67 +0,0 @@
package im.status.hardwallet_lite_android.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class APDUCommand {
protected int cla;
protected int ins;
protected int p1;
protected int p2;
protected int lc;
protected byte[] data;
protected boolean needsLE;
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
this(cla, ins, p1, p2, data, false);
}
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
this.cla = cla;
this.ins = ins;
this.p1 = p1;
this.p2 = p2;
this.data = data;
this.needsLE = needsLE;
}
public byte[] serialize() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(this.cla);
out.write(this.ins);
out.write(this.p1);
out.write(this.p2);
out.write(this.data.length);
out.write(this.data);
if (this.needsLE) {
out.write(0); // Response length
}
return out.toByteArray();
}
public int getCla() {
return cla;
}
public int getIns() {
return ins;
}
public int getP1() {
return p1;
}
public int getP2() {
return p2;
}
public byte[] getData() {
return data;
}
public boolean getNeedsLE() {
return this.needsLE;
}
}
@@ -1,15 +0,0 @@
package im.status.hardwallet_lite_android.io;
public class APDUException extends Exception {
public final int sw;
public APDUException(int sw, String message) {
super(message + ", 0x" + String.format("%04X", sw));
this.sw = sw;
}
public APDUException(String message) {
super(message);
this.sw = 0;
}
}
@@ -1,67 +0,0 @@
package im.status.hardwallet_lite_android.io;
public class APDUResponse {
public static int SW_OK = 0x9000;
public static int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
public static int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
public static int SW_CARD_LOCKED = 0x6283;
public static int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
public static int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
private byte[] apdu;
private byte[] data;
private int sw;
private int sw1;
private int sw2;
public APDUResponse(byte[] apdu) {
if (apdu.length < 2) {
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
}
this.apdu = apdu;
this.parse();
}
private void parse() {
int length = this.apdu.length;
this.sw1 = this.apdu[length - 2] & 0xff;
this.sw2 = this.apdu[length - 1] & 0xff;
this.sw = (this.sw1 << 8) | this.sw2;
this.data = new byte[length - 2];
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
}
public boolean isOK() {
return this.sw == SW_OK;
}
public APDUResponse checkOK() throws APDUException {
if (!isOK()) {
throw new APDUException(this.getSw(), "Unexpected error SW");
}
return this;
}
public byte[] getData() {
return this.data;
}
public int getSw() {
return this.sw;
}
public int getSw1() {
return this.sw1;
}
public int getSw2() {
return this.sw2;
}
public byte[] getBytes() {
return this.apdu;
}
}
@@ -1,25 +0,0 @@
package im.status.hardwallet_lite_android.io;
import android.nfc.tech.IsoDep;
import android.util.Log;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
public class CardChannel {
private static final String TAG = "CardChannel";
private IsoDep isoDep;
public CardChannel(IsoDep isoDep) {
this.isoDep = isoDep;
}
public APDUResponse send(APDUCommand cmd) throws IOException {
byte[] apdu = cmd.serialize();
Log.d(TAG, String.format("COMMAND %s", Hex.toHexString(apdu)));
byte[] resp = this.isoDep.transceive(apdu);
Log.d(TAG, String.format("RESPONSE %s %n-----------------------", Hex.toHexString(resp)));
return new APDUResponse(resp);
}
}
@@ -1,76 +0,0 @@
package im.status.hardwallet_lite_android.io;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.security.Security;
public class CardManager extends Thread implements NfcAdapter.ReaderCallback {
public CardManager() {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
private static final String TAG = "CardManager";
private IsoDep isoDep;
private boolean isRunning;
private OnCardConnectedListener onCardConnectedListener;
public boolean isConnected() {
return isoDep != null && isoDep.isConnected();
}
@Override
public void onTagDiscovered(Tag tag) {
isoDep = IsoDep.get(tag);
try {
isoDep = IsoDep.get(tag);
isoDep.connect();
isoDep.setTimeout(120000);
} catch (IOException e) {
Log.e(TAG, "error connecting to tag");
}
}
public void run() {
boolean connected = isConnected();
while (true) {
boolean newConnected = isConnected();
if (newConnected != connected) {
connected = newConnected;
Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
if (connected && !isRunning) {
onCardConnected();
} else {
onCardDisconnected();
}
}
SystemClock.sleep(50);
}
}
private void onCardConnected() {
isRunning = true;
onCardConnectedListener.onConnected(new CardChannel(isoDep));
isRunning = false;
}
private void onCardDisconnected() {
isRunning = false;
isoDep = null;
}
public void setOnCardConnectedListener(OnCardConnectedListener onConnectedListener) {
onCardConnectedListener = onConnectedListener;
}
}
@@ -1,5 +0,0 @@
package im.status.hardwallet_lite_android.io;
public interface OnCardConnectedListener {
void onConnected(CardChannel channel);
}
@@ -1,542 +0,0 @@
package im.status.hardwallet_lite_android.wallet;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.util.encoders.Hex;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
/**
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
* file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
* pre/post processing.
*/
public class WalletAppletCommandSet {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
static final byte INS_LOAD_KEY = (byte) 0xD0;
static final byte INS_DERIVE_KEY = (byte) 0xD1;
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
static final byte INS_REMOVE_KEY = (byte) 0xD3;
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
public static final byte GET_STATUS_P1_APPLICATION = 0x00;
static final byte LOAD_KEY_P1_EC = 0x01;
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
static final byte LOAD_KEY_P1_SEED = 0x03;
static final byte DERIVE_P1_ASSISTED_MASK = 0x01;
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P2_KEY_PATH = 0x00;
static final byte DERIVE_P2_PUBLIC_KEY = 0x01;
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
static final byte TLV_PUB_KEY = (byte) 0x80;
static final byte TLV_PRIV_KEY = (byte) 0x81;
static final byte TLV_CHAIN_CODE = (byte) 0x82;
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
public static final String APPLET_AID = "53746174757357616C6C6574417070";
public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
public WalletAppletCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
}
public void setSecureChannel(SecureChannelSession secureChannel) {
this.secureChannel = secureChannel;
}
/**
* 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.
*
* @return the raw card response
* @throws IOException communication error
*/
/**
* 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.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES);
APDUResponse resp = apduChannel.send(selectApplet);
if (resp.getSw() == 0x9000) {
byte[] keyData = extractPublicKeyFromSelect(resp.getData());
this.secureChannel = new SecureChannelSession(keyData);
}
return resp;
}
/**
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @return the raw card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel() throws IOException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
/**
* Automatically pairs. Derives the secret from the given password.
*
* @throws IOException communication error
*/
public void autoPair(String pairingPassword) throws IOException {
SecretKey key;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Status Hardware Wallet Lite".getBytes(), 50000, 32 * 8);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?");
}
secureChannel.autoPair(apduChannel, key.getEncoded());
}
/**
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoPair(byte[] sharedSecret) throws IOException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
/**
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoUnpair() throws IOException {
secureChannel.autoUnpair(apduChannel);
}
/**
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate() throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse pair(byte p1, byte[] data) throws IOException {
return secureChannel.pair(apduChannel, p1, data);
}
/**
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse unpair(byte p1) throws IOException {
return secureChannel.unpair(apduChannel, p1);
}
/**
* Unpair all other clients.
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
}
/**
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
* class itself.
*
* @param info the P1 of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse getStatus(byte info) throws IOException {
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
return secureChannel.transmit(apduChannel, getStatus);
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key
* derivation support.
*
* @return whether public key derivation is supported or not
* @throws IOException communication error
*/
public boolean getPublicKeyDerivationSupport() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 1] != 0x00;
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization
* status
*
* @return whether public key derivation is supported or not
* @throws IOException communication error
*/
public boolean getKeyInitializationStatus() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 4] != 0x00;
}
/**
* 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
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse verifyPIN(String pin) throws IOException {
APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes());
return secureChannel.transmit(apduChannel, verifyPIN);
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, String pin) throws IOException {
return changePIN(pinType, pin.getBytes());
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, byte[] pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin);
return secureChannel.transmit(apduChannel, changePIN);
}
/**
* Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure
* channel and used as APDU data.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unblockPIN(String puk, String newPin) throws IOException {
APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
return secureChannel.transmit(apduChannel, unblockPIN);
}
/**
* Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of
* the command is set to LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation.
* The loaded keyset is extended and support further key derivation.
*
* @param aPrivate a private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(PrivateKey aPrivate, byte[] chainCode) throws IOException {
byte[] privateKey = ((ECPrivateKey) aPrivate).getD().toByteArray();
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
byte[] data = new byte[chainCode.length + privLen];
System.arraycopy(privateKey, privOff, data, 0, privLen);
System.arraycopy(chainCode, 0, data, privLen, chainCode.length);
return loadKey(data, LOAD_KEY_P1_SEED);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that
* the card will not be able to do further key derivation.
*
* @param ecKeyPair a key pair
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair ecKeyPair) throws IOException {
return loadKey(ecKeyPair, false, null);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value
* of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically
* to either LOAD_KEY_P1_EC or LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param keyPair a key pair
* @param omitPublicKey whether the public key is sent or not
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws IOException {
byte[] publicKey = omitPublicKey ? null : ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray();
return loadKey(publicKey, privateKey, chainCode);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is
* included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or
* LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param publicKey a raw public key
* @param privateKey a raw private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
int off = 0;
int totalLength = publicKey == null ? 0 : (publicKey.length + 2);
totalLength += (privLen + 2);
totalLength += chainCode == null ? 0 : (chainCode.length + 2);
if (totalLength > 127) {
totalLength += 3;
} else {
totalLength += 2;
}
byte[] data = new byte[totalLength];
data[off++] = (byte) 0xA1;
if (totalLength > 127) {
data[off++] = (byte) 0x81;
data[off++] = (byte) (totalLength - 3);
} else {
data[off++] = (byte) (totalLength - 2);
}
if (publicKey != null) {
data[off++] = TLV_PUB_KEY;
data[off++] = (byte) publicKey.length;
System.arraycopy(publicKey, 0, data, off, publicKey.length);
off += publicKey.length;
}
data[off++] = TLV_PRIV_KEY;
data[off++] = (byte) privLen;
System.arraycopy(privateKey, privOff, data, off, privLen);
off += privLen;
byte p1;
if (chainCode != null) {
p1 = LOAD_KEY_P1_EXT_EC;
data[off++] = (byte) TLV_CHAIN_CODE;
data[off++] = (byte) chainCode.length;
System.arraycopy(chainCode, 0, data, off, chainCode.length);
} else {
p1 = LOAD_KEY_P1_EC;
}
return loadKey(data, p1);
}
/**
* Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1.
*
* @param data key data
* @param keyType the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] data, byte keyType) throws IOException {
APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data);
return secureChannel.transmit(apduChannel, loadKey);
}
/**
* Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1.
*
* @param cs the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse generateMnemonic(int cs) throws IOException {
APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
return secureChannel.transmit(apduChannel, generateMnemonic);
}
/**
* Sends a REMOVE KEY APDU.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse removeKey() throws IOException {
APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]);
return secureChannel.transmit(apduChannel, removeKey);
}
/**
* Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form
* the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is
* generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and
* isLast will always be true at the same time.
*
* @param data the data to sign
* @param dataType the P1 parameter
* @param isFirst whether this is the first block of the command or not
* @param isLast whether this is the last block of the command or not
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws IOException {
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, dataType, p2, data);
return secureChannel.transmit(apduChannel, sign);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 and P2 parameters are forced to 0, meaning
* that the derivation starts from the master key and is non-assisted.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data) throws IOException {
return deriveKey(data, DERIVE_P1_SOURCE_MASTER, false, false);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The reset and assisted parameters are combined to
* form P1. The isPublicKey parameter is used for P2.
*
* @param data the raw key path or a public key
* @param source the source to start derivation
* @param assisted whether we are doing assisted derivation or not
* @param isPublicKey whether we are sending a public key or a key path (only make sense during assisted derivation)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data, int source, boolean assisted, boolean isPublicKey) throws IOException {
byte p1 = assisted ? DERIVE_P1_ASSISTED_MASK : 0;
p1 |= source;
byte p2 = isPublicKey ? DERIVE_P2_PUBLIC_KEY : DERIVE_P2_KEY_PATH;
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, p1, p2, data);
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
/**
* Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself
*
* @param keyPathIndex the P1 parameter
* @param publicOnly the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte keyPathIndex, boolean publicOnly) throws IOException {
byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]);
return secureChannel.transmit(apduChannel, exportKey);
}
/**
* Sends the INIT command to the card.
*
* @param pin the PIN
* @param puk the PUK
* @param sharedSecret the shared secret for pairing
* @return the raw card response
* @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);
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
private byte[] extractPublicKeyFromSelect(byte[] select) {
if (select[0] == TLV_APPLICATION_INFO_TEMPLATE) {
return Arrays.copyOfRange(select, 22, 22 + select[21]);
} else if (select[0] == TLV_PUB_KEY) {
return Arrays.copyOfRange(select, 2, select.length);
} else {
throw new RuntimeException("Unexpected card response");
}
}
}
@@ -0,0 +1,194 @@
package im.status.keycard.applet;
/**
* Parses the response from a SELECT command. If the card has not yet received the INIT command the isInitializedCard
* will return false and only the getSecureChannelPubKey method will return a valid value.
*/
public class ApplicationInfo {
private boolean initializedCard;
private byte[] instanceUID;
private byte[] secureChannelPubKey;
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.
*
* @param tlvData the raw response data from the card
* @throws IllegalArgumentException the TLV does not follow the allowed format
*/
public ApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
int topTag = tlv.readTag();
tlv.unreadLastTag();
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;
}
tlv.enterConstructed(TLV_APPLICATION_INFO_TEMPLATE);
instanceUID = tlv.readPrimitive(TLV_UID);
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
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;
}
/**
* Returns if the card is initialized or not. If this method returns false, only the getSecureChannelPubKey method
* will return a valid value.
*
* @return true if initialized, false otherwise
*/
public boolean isInitializedCard() {
return initializedCard;
}
/**
* Utility method to discover if the card has a master key.
*
* @return true if the card has a master key, false otherwise
*/
public boolean hasMasterKey() {
return keyUID.length != 0;
}
/**
* The instance UID of the applet. This ID never changes for the lifetime of the applet.
*
* @return the instance UID
*/
public byte[] getInstanceUID() {
return instanceUID;
}
/**
* The public key to be used for secure channel opening. Usually handled internally by the KeycardCommandSet.
*
* @return the public key
*/
public byte[] getSecureChannelPubKey() {
return secureChannelPubKey;
}
/**
* 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 getAppVersionString(appVersion);
}
/**
* A formatted application version.
* @return the string representation of the application version
*/
static String getAppVersionString(short appVersion) {
return (appVersion >> 8) + "." + (appVersion & 0xff);
}
/**
* The number of remaining pairing slots. If zero is returned, no further pairing is possible.
* @return the number of remaining pairing slots
*/
public byte getFreePairingSlots() {
return freePairingSlots;
}
/**
* The UID of the master key on this card. Changes every time a different master key is stored. It has zero length if
* no key is on the card.
*
* @return the Key UID.
*/
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;
}
}
@@ -0,0 +1,50 @@
package im.status.keycard.applet;
/**
* Parses the result of a GET STATUS command retrieving application status.
*/
public class ApplicationStatus {
private byte pinRetryCount;
private byte pukRetryCount;
private boolean hasMasterKey;
public static final byte TLV_APPLICATION_STATUS_TEMPLATE = (byte) 0xA3;
/**
* Constructor from TLV data
* @param tlvData the TLV data
* @throws IllegalArgumentException if the TLV does not follow the expected format
*/
public ApplicationStatus(byte[] tlvData) throws IllegalArgumentException {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_APPLICATION_STATUS_TEMPLATE);
pinRetryCount = (byte) tlv.readInt();
pukRetryCount = (byte) tlv.readInt();
hasMasterKey = tlv.readBoolean();
}
/**
* The available PIN retry count.
* @return the available PIN retry count
*/
public byte getPINRetryCount() {
return pinRetryCount;
}
/**
* The available PUK retry count.
* @return the available PUK retry count
*/
public byte getPUKRetryCount() {
return pukRetryCount;
}
/**
* Whether the card has a master key or not.
*
* @return whether the card has a master key or not.
*/
public boolean hasMasterKey() {
return hasMasterKey;
}
}
@@ -0,0 +1,223 @@
package im.status.keycard.applet;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Represents a BIP32 keypair. This can be a master key or any other key in the path. Contains convenience method to
* read and write formats the the card understands.
*/
public class BIP32KeyPair {
private byte[] privateKey;
private byte[] chainCode;
private byte[] publicKey;
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
static final byte TLV_PUB_KEY = (byte) 0x80;
static final byte TLV_PRIV_KEY = (byte) 0x81;
static final byte TLV_CHAIN_CODE = (byte) 0x82;
/**
* Returns a BIP32 keypair from a BIP32 binary seed. It includes all components.
*
* @param binarySeed the binary seed
* @return the BIP32 keypair
*/
public static BIP32KeyPair fromBinarySeed(byte[] binarySeed) {
try {
Mac hmacSHA512 = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec("Bitcoin seed".getBytes(), "HmacSHA512");
hmacSHA512.init(keySpec);
byte[] mac = hmacSHA512.doFinal(binarySeed);
return new BIP32KeyPair(Arrays.copyOf(mac, 32), Arrays.copyOfRange(mac, 32, 64), null);
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle correctly installed? ", e);
}
}
/**
* Constructs a BIP32 keypair from a KEY TEMPLATE TLV. If the data is the output of the EXPORT KEY command, it will
* never contain the chain code field and could contain only the public key.
*
* @param tlvData the TLV data
* @return the BIP32 keypair
*/
public static BIP32KeyPair fromTLV(byte[] tlvData) {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_KEY_TEMPLATE);
byte[] pubKey = null;
byte[] privKey = null;
byte[] chainCode = null;
int tag = tlv.readTag();
if (tag == TLV_PUB_KEY) {
tlv.unreadLastTag();
pubKey = tlv.readPrimitive(TLV_PUB_KEY);
tag = tlv.readTag();
}
if (tag == TLV_PRIV_KEY) {
tlv.unreadLastTag();
privKey = tlv.readPrimitive(TLV_PRIV_KEY);
tag = tlv.readTag();
}
if (tag == TLV_CHAIN_CODE) {
tlv.unreadLastTag();
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
}
return new BIP32KeyPair(privKey, chainCode, pubKey);
}
/**
* Low level constructor. If the private key is not null, the public key can be omitted and it will be calculated
* automatically. If the private key is null the chain code must be null and the public key must be not null.
*
* @param privateKey the private key
* @param chainCode the chain code
* @param publicKey the public key
*/
public BIP32KeyPair(byte[] privateKey, byte[] chainCode, byte[] publicKey) {
if (privateKey == null && (chainCode != null || publicKey == null)) {
throw new IllegalArgumentException("Private key can be null only if the public key is not null and the chain code is null");
}
this.privateKey = privateKey;
this.chainCode = chainCode;
if (publicKey != null) {
this.publicKey = publicKey;
} else {
calculatePublicKey();
}
}
private void calculatePublicKey() {
BigInteger k = new BigInteger(1, this.privateKey);
ECPoint pubKey = RecoverableSignature.CURVE.getG().multiply(k);
this.publicKey = pubKey.getEncoded(false);
}
/**
* Returns the TLV representation of this object.
*
* @return the TLV representation of this object.
*/
public byte[] toTLV() {
return toTLV(true);
}
/**
* Returns the TLV representation of this object, optionally omitting the public component.
*
* @return the TLV representation of this object.
*/
public byte[] toTLV(boolean includePublic) {
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
int off = 0;
int totalLength = includePublic ? (publicKey.length + 2) : 0;
totalLength += (privLen + 2);
totalLength += isExtended() ? (chainCode.length + 2) : 0;
if (totalLength > 127) {
totalLength += 3;
} else {
totalLength += 2;
}
byte[] data = new byte[totalLength];
data[off++] = TLV_KEY_TEMPLATE;
if (totalLength > 127) {
data[off++] = (byte) 0x81;
data[off++] = (byte) (totalLength - 3);
} else {
data[off++] = (byte) (totalLength - 2);
}
if (includePublic) {
data[off++] = TLV_PUB_KEY;
data[off++] = (byte) publicKey.length;
System.arraycopy(publicKey, 0, data, off, publicKey.length);
off += publicKey.length;
}
data[off++] = TLV_PRIV_KEY;
data[off++] = (byte) privLen;
System.arraycopy(privateKey, privOff, data, off, privLen);
off += privLen;
if (isExtended()) {
data[off++] = (byte) TLV_CHAIN_CODE;
data[off++] = (byte) chainCode.length;
System.arraycopy(chainCode, 0, data, off, chainCode.length);
}
return data;
}
/**
* Returns the public key as an Ethereum address.
*
* @return the Ethereum address
*/
public byte[] toEthereumAddress() {
return Ethereum.toEthereumAddress(publicKey);
}
/**
* Returns the private key. Might be null.
*
* @return the private key
*/
public byte[] getPrivateKey() {
return privateKey;
}
/**
* Returns the chain code. Might be null.
*
* @return the chain code
*/
public byte[] getChainCode() {
return chainCode;
}
/**
* Returns the public key. Is never null.
* @return the public key
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* True if only the public key is contained, false otherwise.
* @return true or false
*/
public boolean isPublicOnly() {
return privateKey == null;
}
/**
* True if the chain code is contained, false otherwise.
* @return true or false
*/
public boolean isExtended() {
return chainCode != null;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -0,0 +1,46 @@
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("A0000008040001");
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("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 default instance of the Keycard applet.
*
* @return the instance AID of the Keycard applet
*/
public static byte[] getKeycardInstanceAID() {
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;
}
}
@@ -0,0 +1,145 @@
package im.status.keycard.applet;
import java.util.StringTokenizer;
/**
* Keypath object to be used with the KeycardCommandSet
*/
public class KeyPath {
private int source;
private byte[] data;
/**
* Parses a keypath into a byte array and source parameter to be used with the KeycardCommandSet object.
*
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
*
* The first component can be either "m", indicating the master key, "..", indicating the parent of the current key,
* or "." indicating the current key. It can also be omitted, in which case it is considered the same as being ".".
*
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
* which indicates an hardened key.
*
* An example of a valid path is "m/44'/0'/0'/0/0"
*
*
* @param keypath the keypath as a string
*/
public KeyPath(String keypath) {
StringTokenizer tokenizer = new StringTokenizer(keypath, "/");
String sourceOrFirstElement = tokenizer.nextToken();
switch(sourceOrFirstElement) {
case "m":
source = KeycardCommandSet.DERIVE_P1_SOURCE_MASTER;
break;
case "..":
source = KeycardCommandSet.DERIVE_P1_SOURCE_PARENT;
break;
case ".":
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
break;
default:
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
tokenizer = new StringTokenizer(keypath, "/"); // rewind
break;
}
int componentCount = tokenizer.countTokens();
if (componentCount > 10) {
throw new IllegalArgumentException("Too many components");
}
data = new byte[4 * componentCount];
for (int i = 0; i < componentCount; i++) {
long component = parseComponent(tokenizer.nextToken());
writeComponent(component, i);
}
}
public KeyPath(byte[] data, int source) {
this.data = data;
this.source = source;
}
public KeyPath(byte[] data) {
this(data, KeycardCommandSet.DERIVE_P1_SOURCE_MASTER);
}
private long parseComponent(String num) {
long sign;
if (num.endsWith("'")) {
sign = 0x80000000L;
num = num.substring(0, (num.length() - 1));
} else {
sign = 0L;
}
if (num.startsWith("+") || num.startsWith("-")) {
throw new NumberFormatException("No sign allowed");
}
return (sign | Long.parseLong(num));
}
private void writeComponent(long component, int i) {
int off = (i*4);
data[off] = (byte)((component >> 24) & 0xff);
data[off + 1] = (byte)((component >> 16) & 0xff);
data[off + 2] = (byte)((component >> 8) & 0xff);
data[off + 3] = (byte)(component & 0xff);
}
/**
* The source of the derive command.
*
* @return the source of the derive command
*/
public int getSource() {
return source;
}
/**
* The byte encoded key path.
*
* @return byte encoded key path
*/
public byte[] getData() {
return data;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
switch(source) {
case KeycardCommandSet.DERIVE_P1_SOURCE_MASTER:
sb.append('m');
break;
case KeycardCommandSet.DERIVE_P1_SOURCE_PARENT:
sb.append("..");
break;
case KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT:
sb.append('.');
break;
}
for (int i = 0; i < this.data.length; i += 4) {
sb.append('/');
appendComponent(sb, i);
}
return sb.toString();
}
private void appendComponent(StringBuffer sb, int i) {
int num = ((this.data[i] & 0x7f) << 24) | ((this.data[i+1] & 0xff) << 16) | ((this.data[i+2] & 0xff) << 8) | (this.data[i+3] & 0xff);
sb.append(num);
if ((this.data[i] & 0x80) == 0x80) {
sb.append('\'');
}
}
}
@@ -0,0 +1,890 @@
package im.status.keycard.applet;
import im.status.keycard.io.*;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Arrays;
/**
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
* file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
* pre/post processing.
*/
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;
static final byte INS_LOAD_KEY = (byte) 0xD0;
static final byte INS_DERIVE_KEY = (byte) 0xD1;
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_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;
public static final byte LOAD_KEY_P1_EC = 0x01;
public static final byte LOAD_KEY_P1_EXT_EC = 0x02;
public static final byte LOAD_KEY_P1_SEED = 0x03;
public static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
public static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
public static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
static final byte DUPLICATE_KEY_P1_START = 0x00;
static final byte DUPLICATE_KEY_P1_ADD_ENTROPY = 0x01;
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;
public static final int GENERATE_MNEMONIC_21_WORDS = 0x07;
public static final int GENERATE_MNEMONIC_24_WORDS = 0x08;
static final byte EXPORT_KEY_P1_CURRENT = 0x00;
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
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;
}
/**
* Returns the current pairing data.
*/
public Pairing getPairing() {
return secureChannel.getPairing();
}
/**
* Sets the pairing data.
* @param pairing data from an existing pairing
*/
public void setPairing(Pairing pairing) {
secureChannel.setPairing(pairing);
}
/**
* 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 {
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) {
info = new ApplicationInfo(resp.getData());
if (info.hasSecureChannelCapability()) {
this.secureChannel.generateSecret(info.getSecureChannelPubKey());
this.secureChannel.reset();
}
}
return resp;
}
/**
* 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, APDUException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
/**
* Automatically pairs. Derives the secret from the given password.
*
* @throws IOException communication error
* @throws APDUException pairing error
*/
public void autoPair(String pairingPassword) throws IOException, APDUException {
byte[] secret = pairingPasswordToSecret(pairingPassword);
secureChannel.autoPair(apduChannel, secret);
}
/**
* Converts a pairing password to a binary pairing secret.
*
* @param pairingPassword the pairing password
* @return the pairing secret
*/
public byte[] pairingPasswordToSecret(String pairingPassword) {
SecretKey key;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", "BC");
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?");
}
return key.getEncoded();
}
/**
* 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, APDUException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
/**
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
* @throws APDUException unpairing error
*/
public void autoUnpair() throws IOException, APDUException {
secureChannel.autoUnpair(apduChannel);
}
/**
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate() throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse pair(byte p1, byte[] data) throws IOException {
return secureChannel.pair(apduChannel, p1, data);
}
/**
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse unpair(byte p1) throws IOException {
return secureChannel.unpair(apduChannel, p1);
}
/**
* 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.
*
* @param info the P1 of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse getStatus(byte info) throws IOException {
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
return secureChannel.transmit(apduChannel, getStatus);
}
/**
* 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
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse verifyPIN(String pin) throws IOException {
APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes());
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.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, String pin) throws IOException {
return changePIN(pinType, pin.getBytes());
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pinType the PIN type
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(int pinType, byte[] pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin);
return secureChannel.transmit(apduChannel, changePIN);
}
/**
* Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure
* channel and used as APDU data.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unblockPIN(String puk, String newPin) throws IOException {
APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
return secureChannel.transmit(apduChannel, unblockPIN);
}
/**
* Sends a LOAD KEY APDU. The given seed is sent as-is and the P1 of the command is set to LOAD_KEY_P1_SEED (0x03).
* This works on cards which support public key derivation. The loaded keyset is extended and support further
* key derivation.
*
* @param seed the binary seed
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] seed) throws IOException {
return loadKey(seed, LOAD_KEY_P1_SEED);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that
* the card will not be able to do further key derivation.
*
* @param ecKeyPair a key pair
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair ecKeyPair) throws IOException {
return loadKey(ecKeyPair, false, null);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value
* of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically
* to either LOAD_KEY_P1_EC or LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param keyPair a key pair
* @param omitPublicKey whether the public key is sent or not
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws IOException {
byte[] publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray();
return loadKey(new BIP32KeyPair(privateKey, chainCode, publicKey), omitPublicKey);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is
* included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or
* LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param publicKey a raw public key
* @param privateKey a raw private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
return loadKey(new BIP32KeyPair(privateKey, chainCode, publicKey), publicKey == null);
}
public APDUResponse loadKey(BIP32KeyPair keyPair) throws IOException {
return loadKey(keyPair, false);
}
public APDUResponse loadKey(BIP32KeyPair keyPair, boolean omitPublic) throws IOException {
byte p1;
if (keyPair.isExtended()) {
p1 = LOAD_KEY_P1_EXT_EC;
} else {
p1 = LOAD_KEY_P1_EC;
}
return loadKey(keyPair.toTLV(!omitPublic), p1);
}
/**
* Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1.
*
* @param data key data
* @param keyType the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] data, byte keyType) throws IOException {
APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data);
return secureChannel.transmit(apduChannel, loadKey);
}
/**
* Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1.
*
* @param cs the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse generateMnemonic(int cs) throws IOException {
APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
return secureChannel.transmit(apduChannel, generateMnemonic);
}
/**
* Sends a REMOVE KEY APDU.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse removeKey() throws IOException {
APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]);
return secureChannel.transmit(apduChannel, removeKey);
}
/**
* Sends a GENERATE KEY APDU.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse generateKey() throws IOException {
APDUCommand generateKey = secureChannel.protectedCommand(0x80, INS_GENERATE_KEY, 0, 0, new byte[0]);
return secureChannel.transmit(apduChannel, generateKey);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_CURRENT_KEY);
}
/**
* 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 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 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 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 signPinless(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_PINLESS);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
* a derivation path.
*
* @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, int p1) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x01, data);
return secureChannel.transmit(apduChannel, sign);
}
/**
* Sends a DERIVE KEY APDU with the given key path.
*
* @param keypath the string key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(String keypath) throws IOException {
KeyPath path = new KeyPath(keypath);
return deriveKey(path.getData(), path.getSource());
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
* starts from the master key.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data) throws IOException {
return deriveKey(data, DERIVE_P1_SOURCE_MASTER);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The source parameter is used as P1.
*
* @param data the raw key path or a public key
* @param source the source to start derivation
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data, int source) throws IOException {
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, source, 0x00, data);
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.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
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.
*
* @param publicOnly exports only the public key
* @return the raw card reponse
* @throws IOException communication error
*/
public APDUResponse exportCurrentKey(boolean publicOnly) throws IOException {
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.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param publicOnly the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(String keyPath, boolean makeCurrent, boolean publicOnly) throws IOException {
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.
*
* @param keyPath the keypath to export
* @param makeCurrent if the key should be made current or not
* @param publicOnly the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, boolean publicOnly) throws IOException {
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.
*
* @param derivationOptions the P1 parameter
* @param publicOnly the P2 parameter
* @param keypath the data parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(int derivationOptions, boolean publicOnly, byte[] keypath) throws IOException {
return exportKey(derivationOptions, poToP2(publicOnly), keypath);
}
/**
* 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
* @param pairingPassword pairing password
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
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.
*
* @param pin the PIN
* @param puk the PUK
* @param sharedSecret the shared secret for pairing
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
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);
}
}
@@ -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();
}
}
@@ -0,0 +1,156 @@
package im.status.keycard.applet;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
public class Mnemonic {
private final static int WORDLIST_SIZE = 2048;
private short[] indexes;
private String[] wordlist;
/**
* Constructs a Mnemonic object from the response of the GENERATE MNEMONIC APDU
*
* @param data the card response
*/
public Mnemonic(byte[] data) {
this.indexes = new short[data.length/2];
for (int i = 0; i < this.indexes.length; i++) {
this.indexes[i] = (short) (((data[i * 2] & 0xff) << 8) | (data[(i * 2) + 1] & 0xff));
}
}
/**
* Sets the wordlist, which must be a list of 2048 words.
*
* @param wordlist
*/
public void setWordlist(String[] wordlist) {
if (wordlist.length != WORDLIST_SIZE) {
throw new IllegalArgumentException("The list must contain exactly 2048 entries");
}
this.wordlist = wordlist;
}
/**
* Returns the official BIP39 english wordlist as fetched from https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt on 23 Oct 2019.
*
*/
public void fetchBIP39EnglishWordlist() {
this.wordlist = MnemonicEnglishDictionary.words;
}
/**
* Gets the indexes of all words of the mnemonic.
* @return indexes
*/
public short[] getIndexes() {
return indexes;
}
/**
* The words of the mnemonic phrase. Requires the wordlist to be non-null.
*
* @return the array of words
*/
public String[] getWords() {
if (this.wordlist == null) {
throw new IllegalStateException("The wordlist must be set first");
}
String[] words = new String[this.indexes.length];
for (int i = 0; i < this.indexes.length; i++) {
words[i] = this.wordlist[this.indexes[i]];
}
return words;
}
/**
* The representation of this object as a mnemonic phrase. Requires the wordlist to be non-null.
* @return the mnemonic phrase
*/
public String toMnemonicPhrase() {
return join(" ", getWords());
}
/**
* The binary seed representation of this object, with no password.
*
* @return the binary seed
*/
public byte[] toBinarySeed() {
return toBinarySeed("");
}
/**
* The binary seed representation of this object, with a password.
*
* @param password can be an empty string but not null
* @return the binary seed
*/
public byte[] toBinarySeed(String password) {
return toBinarySeed(toMnemonicPhrase(), password);
}
/**
* The full master key, generated from this mnemonic.
*
*/
public BIP32KeyPair toBIP32KeyPair() {
return toBIP32KeyPair("");
}
/**
* The full master key, generated from this mnemonic with a password.
*
* @param password can be an empty string but not null
* @return the binary seed
*/
public BIP32KeyPair toBIP32KeyPair(String password) {
return BIP32KeyPair.fromBinarySeed(toBinarySeed(password));
}
public static byte[] toBinarySeed(String mnemonicPhrase, String password) {
SecretKey key;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512", "BC");
PBEKeySpec spec = new PBEKeySpec(mnemonicPhrase.toCharArray(), ("mnemonic" + password).getBytes(), 2048, 512);
key = skf.generateSecret(spec);
} catch (Exception e) {
throw new RuntimeException("Is Bouncycastle correctly initialized?", e);
}
return key.getEncoded();
}
/**
* String join. Used instead of Android TextUtils.join or Java 8 String.join method for compatibility reasons.
*
* @param list the list of words
* @param conjunction the conjunction
*
* @return the joined string
*/
private String join(String conjunction, String[] list) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String item : list) {
if (first) {
first = false;
} else {
sb.append(conjunction);
}
sb.append(item);
}
return sb.toString();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
package im.status.keycard.applet;
import org.bouncycastle.util.encoders.Base64;
import java.util.Arrays;
/**
* Stores pairing information.
*/
public class Pairing {
private byte[] pairingKey;
private byte pairingIndex;
/**
* Constructor. The pairingKey and pairingIndex are those generated at the end of a successful pairing.
* @param pairingKey the pairing key
* @param pairingIndex the pairing index
*/
public Pairing(byte[] pairingKey, byte pairingIndex) {
this.pairingKey = pairingKey;
this.pairingIndex = pairingIndex;
}
/**
* Constructor. Initializes from a byte array previously generated from the toByteArray method
* @param fromByteArray the result of a previous toByteArray invocation
*/
public Pairing(byte[] fromByteArray) {
pairingIndex = fromByteArray[0];
pairingKey = Arrays.copyOfRange(fromByteArray, 1, fromByteArray.length);
}
/**
* Constructor. Initializes from a String previously generated from the toBase64 method
* @param base64 the result of a previous toBase64 invocation
*/
public Pairing(String base64) {
this(Base64.decode(base64));
}
public byte[] getPairingKey() {
return pairingKey;
}
public byte getPairingIndex() {
return pairingIndex;
}
public byte[] toByteArray() {
byte[] res = new byte[pairingKey.length + 1];
res[0] = pairingIndex;
System.arraycopy(pairingKey, 0, res, 1, pairingKey.length);
return res;
}
public String toBase64() {
return Base64.toBase64String(toByteArray());
}
}
@@ -0,0 +1,191 @@
package im.status.keycard.applet;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointUtil;
import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Signature with recoverable public key.
*/
public class RecoverableSignature {
private byte[] publicKey;
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_RAW_SIGNATURE = (byte) 0x80;
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
static final ECDomainParameters CURVE;
static {
FixedPointUtil.precompute(CURVE_PARAMS.getG());
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
}
/**
* Parses a signature from the card and calculates the recovery ID.
*
* @param hash the message being signed
* @param tlvData the signature as returned from the card
*/
public RecoverableSignature(byte[] hash, byte[] tlvData) {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
int tag = tlv.readTag();
tlv.unreadLastTag();
if (tag == TLV_RAW_SIGNATURE) {
initFromRawSignature(hash, tlv.readPrimitive(tag));
} else if (tag == TLV_SIGNATURE_TEMPLATE) {
initFromLegacy(hash, tlv);
} else {
throw new IllegalArgumentException("invalid tlv");
}
}
private void initFromLegacy(byte[] hash, TinyBERTLV tlv) {
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
this.publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
this.r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
this.compressed = false;
calculateRecID(hash);
}
private void initFromRawSignature(byte[] hash, byte[] signature) {
this.r = Arrays.copyOfRange(signature, 0, 32);
this.s = Arrays.copyOfRange(signature, 32, 64);
this.recId = signature[64];
this.compressed = false;
this.publicKey = recoverFromSignature(this.recId, hash, this.r, this.s, this.compressed);
}
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, hash, r, s, compressed);
if (Arrays.equals(candidate, publicKey)) {
recId = i;
break;
}
}
if (recId == -1) {
throw new IllegalArgumentException("Unrecoverable signature, cannot find recId");
}
}
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.
*
* @return the public key associated to this signature
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* The recovery ID
*
* @return recovery ID
*/
public int getRecId() {
return recId;
}
/**
* The R value.
*
* @return r
*/
public byte[] getR() {
return r;
}
/**
* The S value
* @return s
*/
public byte[] getS() {
return 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));
BigInteger prime = SecP256K1Curve.q;
if (x.compareTo(prime) >= 0) {
return null;
}
ECPoint R = decompressKey(x, (recId & 1) == 1);
if (!R.multiply(n).isInfinity()) {
return null;
}
BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
BigInteger rInv = r.modInverse(n);
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(compressed);
}
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
X9IntegerConverter x9 = new X9IntegerConverter();
byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve()));
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
return CURVE.getCurve().decodePoint(compEnc);
}
}
@@ -1,16 +1,16 @@
package im.status.hardwallet_lite_android.wallet;
package im.status.keycard.applet;
import im.status.hardwallet_lite_android.io.APDUCommand;
import im.status.hardwallet_lite_android.io.APDUException;
import im.status.hardwallet_lite_android.io.APDUResponse;
import im.status.hardwallet_lite_android.io.CardChannel;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.macs.CBCBlockCipherMac;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.jce.spec.ECPublicKeySpec;
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 org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
@@ -42,9 +42,8 @@ public class SecureChannelSession {
private byte[] secret;
private byte[] publicKey;
private byte[] pairingKey;
private byte[] iv;
private byte pairingIndex;
private Pairing pairing;
private Cipher sessionCipher;
private CBCBlockCipherMac sessionMac;
private SecretKeySpec sessionEncKey;
@@ -53,32 +52,33 @@ public class SecureChannelSession {
private boolean open;
/**
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
* The public key of the card is used as input for the EC-DH algorithm. The output is stored as the secret.
*
* @param keyData the public key returned by the applet as response to the SELECT command
* Constructs a SecureChannel session on the client.
*/
public SecureChannelSession(byte[] keyData) {
public SecureChannelSession() {
random = new SecureRandom();
generateSecret(keyData);
open = false;
}
/**
* Generates a pairing secret. This should be called before each session. The public key of the card is used as input
* for the EC-DH algorithm. The output is stored as the secret.
*
* @param keyData the public key returned by the applet as response to the SELECT command
*/
public void generateSecret(byte[] keyData) {
try {
random = new SecureRandom();
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
g.initialize(ecSpec, random);
KeyPair keyPair = g.generateKeyPair();
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(keyPair.getPrivate());
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
keyAgreement.doPhase(cardKey, true);
secret = keyAgreement.generateSecret();
@@ -96,11 +96,19 @@ public class SecureChannelSession {
}
/**
* Returns the pairing index
* @return the pairing index
* Returns the pairing information
* @return the pairing information
*/
public byte getPairingIndex() {
return pairingIndex;
public Pairing getPairing() {
return pairing;
}
/**
* Sets pairing information needed to open a secure channel.
* @param pairing the pairing information
*/
public void setPairing(Pairing pairing) {
this.pairing = pairing;
}
/**
@@ -108,27 +116,16 @@ public class SecureChannelSession {
* Follows the specifications from the SECURE_CHANNEL.md document.
*
* @param apduChannel the apdu channel
* @return the card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
APDUResponse response = openSecureChannel(apduChannel, pairingIndex, publicKey);
if (response.getSw() != 0x9000) {
throw new IOException("OPEN SECURE CHANNEL failed");
}
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException, APDUException {
APDUResponse response = openSecureChannel(apduChannel, pairing.getPairingIndex(), publicKey);
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);
}
/**
@@ -140,14 +137,14 @@ public class SecureChannelSession {
try {
MessageDigest md = MessageDigest.getInstance("SHA512");
md.update(secret);
md.update(pairingKey);
md.update(pairing.getPairingKey());
byte[] data = response.getData();
byte[] keyData = md.digest(Arrays.copyOf(data, SC_SECRET_LENGTH));
iv = Arrays.copyOfRange(data, SC_SECRET_LENGTH, data.length);
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SC_SECRET_LENGTH), "AES");
sessionMacKey = new KeyParameter(keyData, SC_SECRET_LENGTH, SC_SECRET_LENGTH);
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
open = true;
} catch(Exception e) {
@@ -161,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");
}
}
/**
@@ -171,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);
@@ -188,7 +183,7 @@ public class SecureChannelSession {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
md = MessageDigest.getInstance("SHA256", "BC");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
@@ -197,22 +192,16 @@ 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);
pairingKey = md.digest(Arrays.copyOfRange(respData, 1, respData.length));
pairingIndex = respData[0];
pairing = new Pairing(md.digest(Arrays.copyOfRange(respData, 1, respData.length)), respData[0]);
}
/**
@@ -221,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, pairingIndex);
if (resp.getSw() != 0x9000) {
throw new IOException("Unpairing failed");
}
public void autoUnpair(CardChannel apduChannel) throws IOException, APDUException {
unpair(apduChannel, pairing.getPairingIndex()).checkOK("Unpairing failed");
}
/**
@@ -281,8 +266,8 @@ public class SecureChannelSession {
* @throws IOException communication error
*/
public APDUResponse pair(CardChannel apduChannel, byte p1, byte[] data) throws IOException {
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
return transmit(apduChannel, openSecureChannel);
APDUCommand pair = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
return transmit(apduChannel, pair);
}
/**
@@ -294,22 +279,21 @@ public class SecureChannelSession {
* @throws IOException communication error
*/
public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
return transmit(apduChannel, openSecureChannel);
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
return transmit(apduChannel, unpair);
}
/**
* Unpair all other clients
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
for (int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) {
if (i != pairingIndex) {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
transmit(apduChannel, openSecureChannel).checkOK();
if (i != pairing.getPairingIndex()) {
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
transmit(apduChannel, unpair).checkOK();
}
}
}
@@ -434,7 +418,7 @@ public class SecureChannelSession {
random.nextBytes(iv);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionEncKey = new SecretKeySpec(secret, "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
initData = sessionCipher.doFinal(initData);
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
@@ -453,7 +437,7 @@ public class SecureChannelSession {
* would only make things wrong.
*
*/
void setOpen() {
protected void setOpen() {
open = true;
}
@@ -0,0 +1,167 @@
package im.status.keycard.applet;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/**
* Tiny BER-TLV implementation. Not for general usage, but fast and easy to use for this project.
*/
public class TinyBERTLV {
public static final byte TLV_BOOL = (byte) 0x01;
public static final byte TLV_INT = (byte) 0x02;
public static final int END_OF_TLV = (int) 0xffffffff;
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;
}
/**
* Enters a constructed TLV with the given tag
*
* @param tag the tag to enter
* @return the length of the TLV
* @throws IllegalArgumentException if the next tag does not match the given one
*/
public int enterConstructed(int tag) throws IllegalArgumentException {
checkTag(tag, readTag());
return readLength();
}
/**
* Reads a primitive TLV with the given tag
*
* @param tag the tag to read
* @return the body of the TLV
* @throws IllegalArgumentException if the next tag does not match the given one
*/
public byte[] readPrimitive(int tag) throws IllegalArgumentException {
checkTag(tag, readTag());
int len = readLength();
pos += len;
return Arrays.copyOfRange(buffer, (pos - len), pos);
}
/**
* Reads a boolean TLV.
*
* @return the boolean value of the TLV
* @throws IllegalArgumentException if the next tag is not a boolean
*/
public boolean readBoolean() throws IllegalArgumentException {
byte[] val = readPrimitive(TLV_BOOL);
return ((val[0] & 0xff) == 0xff);
}
/**
* Reads an integer TLV.
*
* @return the integer value of the TLV
* @throws IllegalArgumentException if the next tlv is not an integer or is of unsupported length
*/
public int readInt() throws IllegalArgumentException {
byte[] val = readPrimitive(TLV_INT);
return TinyBERTLV.readVal(val, 0, val.length);
}
/**
* Returns all unread bytes in the TLV.
*
* @return all unread bytes
*/
byte[] peekUnread() {
return Arrays.copyOfRange(buffer, pos, buffer.length);
}
/**
* Low-level method to unread the last read tag. Only valid if the previous call was readTag(). Does nothing if the
* end of the TLV has been reached.
*/
public void unreadLastTag() {
if (pos < buffer.length) {
pos--;
}
}
/**
* Reads the next tag. The current implementation only reads tags on one byte. Can be extended if needed.
*
* @return the tag
*/
public int readTag() {
return (pos < buffer.length) ? buffer[pos++] : END_OF_TLV;
}
/**
* Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed.
*
* @return the tag
*/
public int readLength() {
int[] len = TinyBERTLV.readNum(buffer, pos);
pos = len[1];
return len[0];
}
private void checkTag(int expected, int actual) throws IllegalArgumentException {
if (expected != actual) {
unreadLastTag();
throw new IllegalArgumentException("Expected tag: " + expected + ", received: " + actual);
}
}
}
@@ -0,0 +1,244 @@
package im.status.keycard.globalplatform;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Crypto utilities for Global Platform.
*/
public class Crypto {
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
private static boolean bouncyCastleLoaded = false;
public static void addBouncyCastleProvider() {
if (!bouncyCastleLoaded) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
bouncyCastleLoaded = true;
}
}
/**
* Derives a session key for SCP02.
*
* @param cardKey the key to derive
* @param seq the sequence number
* @param purposeData purpose data
*
* @return the derived key
*/
public static byte[] deriveSCP02SessionKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
byte[] key24 = resizeKey24(cardKey);
try {
byte[] derivationData = new byte[16];
// 2 bytes constant
System.arraycopy(purposeData, 0, derivationData, 0, 2);
// 2 bytes sequence counter + 12 bytes 0x00
System.arraycopy(seq, 0, derivationData, 2, 2);
SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8));
return cipher.doFinal(derivationData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException("error generating session keys.", e);
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("error generating session keys.", e);
} catch (NoSuchProviderException e) {
throw new RuntimeException("BouncyCastle not installed");
}
}
/**
* Padding for SCP02 encryption.
*
* @param data data to pad
* @return the padded data
*/
public static byte[] appendDESPadding(byte[] data) {
int paddingLength = 8 - (data.length % 8);
byte[] newData = new byte[data.length + paddingLength];
System.arraycopy(data, 0, newData, 0, data.length);
newData[data.length] = (byte)0x80;
return newData;
}
/**
* Verifies a card cryptogram received using during SCP02 channel establishment.
*
* @param key the key
* @param hostChallenge host challenge
* @param cardChallenge card challenge
* @param cardCryptogram cryptogram to verify
* @return true if correct, false otherwise
*/
public static boolean verifyCryptogram(byte[] key, byte[] hostChallenge, byte[] cardChallenge, byte[] cardCryptogram) {
byte[] data = new byte[hostChallenge.length + cardChallenge.length];
System.arraycopy(hostChallenge, 0, data, 0, hostChallenge.length);
System.arraycopy(cardChallenge, 0, data, hostChallenge.length, cardChallenge.length);
byte[] paddedData = appendDESPadding(data);
byte[] calculated = mac3des(key, paddedData, NullBytes8);
return Arrays.equals(calculated , cardCryptogram);
}
/**
* Calculates a 3DES MAC for SCP02 channel establishment
*
* @param keyData key
* @param data data to sign
* @param iv IV
* @return the MAC
*/
public static byte[] mac3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data, 0, 24);
byte[] tail = new byte[8];
System.arraycopy(result, 16, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error calculating mac.", e);
}
}
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
*
* @param keyData key
* @param data data to sign
* @param iv IV
* @return the MAC
*/
public static byte[] macFull3des(byte[] keyData, byte[] data, byte[] iv) {
try {
SecretKeySpec keyDes = new SecretKeySpec(resizeKey8(keyData), "DES");
Cipher cipherDes = Cipher.getInstance("DES/CBC/NoPadding", "BC");
cipherDes.init(Cipher.ENCRYPT_MODE, keyDes, new IvParameterSpec(iv));
SecretKeySpec keyDes3 = new SecretKeySpec(resizeKey24(keyData), "DESede");
Cipher cipherDes3 = Cipher.getInstance("DESede/CBC/NoPadding", "BC");
byte[] des3Iv = iv.clone();
if (data.length > 8) {
byte[] tmp = cipherDes.doFinal(data, 0, data.length - 8);
System.arraycopy(tmp, tmp.length - 8, des3Iv, 0, 8);
}
cipherDes3.init(Cipher.ENCRYPT_MODE, keyDes3, new IvParameterSpec(des3Iv));
byte[] result = cipherDes3.doFinal(data, data.length - 8, 8);
byte[] tail = new byte[8];
System.arraycopy(result, result.length - 8, tail, 0, 8);
return tail;
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating full triple DES MAC.", e);
}
}
/**
* Used during key derivation .
*
* @param keyData the key data
*
* @return the resized key
*/
private static byte[] resizeKey24(byte[] keyData) {
byte[] key = new byte[24];
System.arraycopy(keyData, 0, key, 0, 16);
System.arraycopy(keyData, 0, key, 16, 8);
return key;
}
/**
* Used during MAC generation.
*
* @param keyData the key data
*
* @return the resized key
*/
private static byte[] resizeKey8(byte[] keyData) {
byte[] key = new byte[8];
System.arraycopy(keyData, 0, key, 0, 8);
return key;
}
/**
* Encrypts the ICV
*
* @param macKeyData MAC Key
* @param mac mac
*
* @return encrypted ICV
*/
public static byte[] encryptICV(byte[] macKeyData, byte[] mac) {
try {
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding", "BC");
SecretKeySpec key = new SecretKeySpec(resizeKey8(macKeyData), "DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(mac);
} catch (GeneralSecurityException e) {
throw new RuntimeException("error generating ICV.", e);
}
}
/**
* Generates the given number of random bytes.
*
* @param length the number of bytes to generate
* @return random bytes
*/
public static byte[] randomBytes(int length) {
SecureRandom random = new SecureRandom();
byte data[] = new byte[length];
random.nextBytes(data);
return data;
}
/**
* Generates a random long between 0 and then given boundary
*
* @param bound the maximum value to generate
* @return the random number
*/
public static long randomLong(long bound) {
SecureRandom random = new SecureRandom();
return Math.abs(random.nextLong()) % bound;
}
}
@@ -0,0 +1,480 @@
package im.status.keycard.globalplatform;
import im.status.keycard.applet.Identifiers;
import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
/**
* Command set used for loading, installing and removing applets and packages. This class is generic and can work with
* any package and applet, but utility methods specific to the Keycard have been provided.
*/
public class GlobalPlatformCommandSet {
static final byte INS_SELECT = (byte) 0xA4;
static final byte INS_INITIALIZE_UPDATE = (byte) 0x50;
static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82;
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;
static final byte INSTALL_FOR_LOAD_P1 = (byte) 0x02;
static final byte INSTALL_FOR_INSTALL_P1 = (byte) 0x0C;
static final byte LOAD_P1_MORE_BLOCKS = (byte) 0x00;
static final byte LOAD_P1_LAST_BLOCK = (byte) 0x80;
private final CardChannel apduChannel;
private SecureChannel secureChannel;
private SCP02Keys cardKeys;
private Session session;
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.
*
* @param apduChannel the channel to the card
*/
public GlobalPlatformCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
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);;
}
/**
* Selects the ISD of the card.
*
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
APDUCommand cmd = new APDUCommand(0x00, INS_SELECT, SELECT_P1_BY_NAME, 0, new byte[0]);
return apduChannel.send(cmd);
}
/**
* Sends an INITIALIZE UPDATE command. Use the openSecureChannel method instead of calling this directly, unless you
* need to use a specific host challenge.
*
* @param hostChallenge the host challenge.
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse initializeUpdate(byte[] hostChallenge) throws IOException, APDUException {
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
APDUResponse resp = apduChannel.send(cmd);
if (resp.isOK()) {
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());
}
return resp;
}
/**
* Sends an EXTERNAL AUTHENTICATE command. Use the openSecureChannel method instead of calling this directly, unless you
* need to use a specific host challenge.
*
* @param hostChallenge the host challenge.
* @return the card response
*
* @throws IOException communication error
*/
public APDUResponse externalAuthenticate(byte[] hostChallenge) throws IOException {
byte[] cardChallenge = this.session.getCardChallenge();
byte[] data = new byte[cardChallenge.length + hostChallenge.length];
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
byte[] paddedData = Crypto.appendDESPadding(data);
byte[] hostCryptogram = Crypto.mac3des(this.session.getKeys().encKeyData, paddedData, Crypto.NullBytes8);
APDUCommand cmd = new APDUCommand(0x84, INS_EXTERNAL_AUTHENTICATE, EXTERNAL_AUTHENTICATE_P1, 0, hostCryptogram);
return this.secureChannel.send(cmd);
}
/**
* 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);
}
/**
* Deletes the Keycard applet instance.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteKeycardInstance() throws IOException {
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.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteNDEFInstance() throws IOException {
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.
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse deleteKeycardPackage() throws IOException {
return delete(Identifiers.PACKAGE_AID);
}
/**
* Deletes the Keycard package and all applets installed from it. This is the method to use to remove a Keycard
* installation.
*
* @throws APDUException one of the DELETE commands failed
* @throws IOException communication error
*/
public void deleteKeycardInstancesAndPackage() throws IOException, APDUException {
delete(Identifiers.PACKAGE_AID, (byte) 0x80).checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND);
}
/**
* Sends a DELETE APDU with the given AID
* @param aid the AID to the delete
* @return the raw card response
*
* @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, p2, data);
return this.secureChannel.send(cmd);
}
/**
* Loads the Keycard package.
*
* @param in the CAP file as an InputStream
* @param cb the progress callback
*
* @throws IOException communication error
* @throws APDUException one of the INSTALL [for Load] or LOAD commands failed
*/
public void loadKeycardPackage(InputStream in, LoadCallback cb) throws IOException, APDUException {
installForLoad(Identifiers.PACKAGE_AID).checkOK();
Load load = new Load(in);
byte[] block;
int steps = load.blocksCount();
while((block = load.nextDataBlock()) != null) {
load(block, (load.getCount() - 1), load.hasMore()).checkOK();
cb.blockLoaded(load.getCount(), steps);
}
}
/**
* Sends an INSTALL [for LOAD] APDU. Use only if loading something other than the Keycard package.
*
* @param aid the AID
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForLoad(byte[] aid) throws IOException {
return installForLoad(aid, new byte[0]);
}
/**
* Sends an INSTALL [for LOAD] APDU with package extradition. Use only if loading something other than the Keycard package.
*
* @param aid the AID
* @param sdaid the AID of the SD target of the extradition
*
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForLoad(byte[] aid, byte[] sdaid) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(aid.length);
data.write(aid);
data.write(sdaid.length);
data.write(sdaid);
// empty hash length and hash
data.write(0x00);
data.write(0x00);
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_LOAD_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* Sends a single LOAD APDU. Use only if loading something other than the Keycard package.
*
* @param data the data of the block
* @param count the block number
* @param hasMoreBlocks whether there are more blocks coming or not
* @return the card response
* @throws IOException communication error
*/
public APDUResponse load(byte[] data, int count, boolean hasMoreBlocks) throws IOException {
int p1 = hasMoreBlocks ? LOAD_P1_MORE_BLOCKS : LOAD_P1_LAST_BLOCK;
APDUCommand cmd = new APDUCommand(0x80, INS_LOAD, p1, count, data);
return this.secureChannel.send(cmd);
}
/**
* Sends an INSTALL [for Install & Make Selectable] command. Use only if not installing applets part of the Keycard
* package
*
* @param packageAID the package AID
* @param appletAID the applet AID
* @param instanceAID the instance AID
* @param params the installation parameters
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(packageAID.length);
data.write(packageAID);
data.write(appletAID.length);
data.write(appletAID);
data.write(instanceAID.length);
data.write(instanceAID);
byte[] privileges = new byte[]{0x00};
data.write(privileges.length);
data.write(privileges);
byte[] fullParams = new byte[2 + params.length];
fullParams[0] = (byte) 0xC9;
fullParams[1] = (byte) params.length;
System.arraycopy(params, 0, fullParams, 2, params.length);
data.write(fullParams.length);
data.write(fullParams);
// empty perform token
data.write(0x00);
APDUCommand cmd = new APDUCommand(0x80, INS_INSTALL, INSTALL_FOR_INSTALL_P1, 0, data.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* Installs the NDEF applet from the Keycard package.
*
* @param ndefRecord the initial NDEF record. Can be a zero-length array but not null
* @return the card response
* @throws IOException communication error
*/
public APDUResponse installNDEFApplet(byte[] ndefRecord) throws IOException {
return installForInstall(Identifiers.PACKAGE_AID, Identifiers.NDEF_AID, Identifiers.NDEF_INSTANCE_AID, ndefRecord);
}
/**
* Installs the Keycard applet.
*
* @return the card response
* @throws IOException communication error.
*/
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]);
}
}
@@ -0,0 +1,189 @@
package im.status.keycard.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* A loadable CAP file.
*/
public class Load {
static final byte CLA = (byte) 0x80;
static final byte INS = (byte) 0xE8;
static final int BLOCK_SIZE = 247; // 255 - 8 bytes for MAC
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
private int offset;
private int count;
private byte[] fullData;
/**
* Reads a CAP file from the given input stream.
*
* @param in the inpu stream
* @throws FileNotFoundException
* @throws IOException
*/
public Load(InputStream in) throws FileNotFoundException, IOException {
this.offset = 0;
this.count = 0;
Map<String, byte[]> files = this.loadFiles(in);
in.close();
this.fullData = this.getCode(files);
}
/**
* Reads the components of the CAP file
* @param in the input stream
* @return the map of component name and values
*
* @throws IOException IO error
*/
private Map<String, byte[]> loadFiles(InputStream in) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
ZipInputStream zip = new ZipInputStream(in);
ZipEntry entry = zip.getNextEntry();
while (entry != null) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count;
while ((count = zip.read(buf)) != -1) {
data.write(buf, 0, count);
}
String name = baseName(entry.getName());
files.put(name, data.toByteArray());
entry = zip.getNextEntry();
}
return files;
}
/**
* The basename of the zip entry
* @param path the path
* @return the base name
*/
private String baseName(String path) {
String[] parts = path.split("[/.]");
return parts[parts.length - 2];
}
/**
* Counts the number of blocks needed to load the entire file. Keeps in account the overhead of SCP02 secure channel
*
* @return the block count
*/
public int blocksCount() {
return (int) Math.ceil(this.fullData.length / (float) BLOCK_SIZE);
}
/**
* Returns the next data block
*
* @return the data block
*/
public byte[] nextDataBlock() {
if (this.offset >= this.fullData.length) {
return null;
}
int rangeEnd = this.offset + BLOCK_SIZE;
if (rangeEnd >= this.fullData.length) {
rangeEnd = this.fullData.length;
}
int size = rangeEnd - offset;
byte[] data = new byte[size];
System.arraycopy(this.fullData, this.offset, data, 0, size);
this.count++;
this.offset += size;
return data;
}
/**
* True if more blocks are present, false otherwise.
*
* @return true if more blocks are present, false otherwise.
*/
public boolean hasMore() {
return this.offset < this.fullData.length;
}
/**
* Encodes the length of the load TLV component
*
* @param length the length as integer
* @return the length encoded as for BER-TLV
*/
private byte[] encodeFullLength(int length) {
if (length < 0x80) {
return new byte[]{(byte) length};
} else if (length < 0xFF) {
return new byte[]{(byte) 0x81, (byte) length};
} else if (length < 0xFFFF) {
return new byte[]{
(byte) 0x82,
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
} else {
return new byte[]{
(byte) 0x83,
(byte) ((length & 0xFF0000) >> 16),
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
}
}
/**
* Serializes the CAP section in a single block.
*
* @param files the components to serialize
* @return the serialized load file
*
*/
private byte[] getCode(Map<String, byte[]> files) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
for (String name : fileNames) {
byte[] fileData = files.get(name);
if (fileData == null) {
continue;
}
dataStream.write(fileData);
}
byte[] data = dataStream.toByteArray();
byte[] encodedFullLength = encodeFullLength(data.length);
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
fullData[0] = (byte) 0xC4;
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
return fullData;
}
/**
* Returns the current block number
*
* @return the current block number
*/
public int getCount() {
return count;
}
}
@@ -0,0 +1,14 @@
package im.status.keycard.globalplatform;
/**
* Callback interface using during package loading process.
*/
public interface LoadCallback {
/**
* Called when a block is loaded.
*
* @param loadedBlock The number of the loaded block (1 based)
* @param blockCount the total number of blocks.
*/
void blockLoaded(int loadedBlock, int blockCount);
}
@@ -0,0 +1,49 @@
package im.status.keycard.globalplatform;
/**
* Keeps keys for SCP02.
*/
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, byte[] dekKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
this.dekKeyData = dekKeyData;
}
/**
* The encryption key
* @return the encryption key
*/
public byte[] getEncKeyData() {
return encKeyData;
}
/**
* The MAC key
*
* @return the MAC key
*/
public byte[] getMacKeyData() {
return macKeyData;
}
/**
* The DEK key
*
* @return the DEK key
*/
public byte[] getDekKeyData() {
return dekKeyData;
}
}
@@ -0,0 +1,73 @@
package im.status.keycard.globalplatform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import im.status.keycard.io.APDUCommand;
/**
* Adds a SCP02 MAC to APDUs.
*/
public class SCP02Wrapper {
private byte[] macKeyData;
private byte[] icv;
/**
* Constructs a new SCP02Wrapper.
*
* @param macKeyData the MAC key
*/
public SCP02Wrapper(byte[] macKeyData) {
this.macKeyData = macKeyData;
this.icv = Crypto.NullBytes8.clone();
}
/**
* Wraps an APDU with SCP02 MAC
* @param cmd the APDU to wrap
* @return the wrapped APDU
*/
public APDUCommand wrap(APDUCommand cmd) {
try {
int cla = (cmd.getCla() | 0x04) & 0xff;
byte[] data = cmd.getData();
ByteArrayOutputStream macData = new ByteArrayOutputStream();
macData.write(cla);
macData.write(cmd.getIns());
macData.write(cmd.getP1());
macData.write(cmd.getP2());
macData.write(data.length + 8);
macData.write(data);
byte[] icv;
if (Arrays.equals(this.icv, Crypto.NullBytes8)) {
icv = this.icv;
} else {
icv = Crypto.encryptICV(this.macKeyData, this.icv);
}
byte[] mac = Crypto.macFull3des(this.macKeyData, Crypto.appendDESPadding(macData.toByteArray()), icv);
byte[] newData = new byte[data.length + mac.length];
System.arraycopy(data, 0, newData, 0, data.length );
System.arraycopy(mac, 0, newData, data.length, mac.length );
APDUCommand wrapped = new APDUCommand(cla, cmd.getIns(), cmd.getP1(), cmd.getP2(), newData, cmd.getNeedsLE());
this.icv = mac.clone();
return wrapped;
} catch (IOException e) {
throw new RuntimeException("error wrapping APDU command.", e);
}
}
/**
* Returns the ICV
* @return the ICV
*/
public byte[] getICV() {
return this.icv;
}
}
@@ -0,0 +1,91 @@
package im.status.keycard.globalplatform;
import java.io.IOException;
import im.status.keycard.io.APDUCommand;
import im.status.keycard.io.APDUException;
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 secure channel.
*/
public class SecureChannel {
private CardChannel channel;
private SCP02Wrapper wrapper;
public static byte[] DERIVATION_PURPOSE_ENC = new byte[]{(byte) 0x01, (byte) 0x82};
public static byte[] DERIVATION_PURPOSE_MAC = new byte[]{(byte) 0x01, (byte) 0x01};
public static byte[] DERIVATION_PURPOSE_DEK = new byte[]{(byte) 0x01, (byte) 0x81};
/**
* Constructs an SCP02 secure channel, wrapping a regular CardChannel.
*
* @param channel the channel to wrap
* @param keys the keys
*/
public SecureChannel(CardChannel channel, SCP02Keys keys) {
this.channel = channel;
this.wrapper = new SCP02Wrapper(keys.getMacKeyData());
}
/**
* Protects the given command with SCP02 and forwards it to the underlying CardChannel.
*
* @param cmd the command to send
* @return the response from the card
*
* @throws IOException communication error
*/
public APDUResponse send(APDUCommand cmd) throws IOException {
APDUCommand wrappedCommand = this.wrapper.wrap(cmd);
return this.channel.send(wrappedCommand);
}
/**
* Verifies the card challenge and builds an SCP02 session object.
*
* @param hostChallenge the host challenge
* @param cardKeys the SCP02 keys
* @param resp the response from the card to the INITIALIZE UPDATE oommand
* @return the Session object built on succesful verification
* @throws APDUException communication error
*/
public static Session verifyChallenge(byte[] hostChallenge, SCP02Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
throw new APDUException(resp.getSw(), "security condition not satisfied");
}
if (resp.getSw() == APDUResponse.SW_AUTHENTICATION_METHOD_BLOCKED) {
throw new APDUException(resp.getSw(), "authentication method blocked");
}
byte[] data = resp.getData();
if (data.length != 28) {
throw new APDUException(resp.getSw(), String.format("bad data length, expected 28, got %d", data.length));
}
byte[] cardChallenge = new byte[8];
System.arraycopy(data, 12, cardChallenge, 0, 8);
byte[] cardCryptogram = new byte[8];
System.arraycopy(data, 20, cardCryptogram, 0, 8);
byte[] seq = new byte[2];
System.arraycopy(data, 12, seq, 0, 2);
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, sessionDekKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
throw new APDUException("error verifying card cryptogram.");
}
return new Session(sessionKeys, cardChallenge);
}
}
@@ -0,0 +1,55 @@
package im.status.keycard.globalplatform;
/**
* SCP02 Session.
*/
public class Session {
private SCP02Keys keys;
private byte[] cardChallenge;
private boolean fallbackKeys;
/**
* Constructs the SCP02 session.
*
* @param keys the session keys
* @param cardChallenge the card challenge
*/
public Session(SCP02Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
this.fallbackKeys = false;
}
/**
* The SCP02 keys
* @return SCP02 keys
*/
public SCP02Keys getKeys() {
return keys;
}
/**
* The card challenge
* @return card challenge
*/
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;
}
}
@@ -0,0 +1,125 @@
package im.status.keycard.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* ISO7816-4 APDU.
*/
public class APDUCommand {
protected int cla;
protected int ins;
protected int p1;
protected int p2;
protected int lc;
protected byte[] data;
protected boolean needsLE;
/**
* Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
*
* @param cla class byte
* @param ins instruction code
* @param p1 P1 parameter
* @param p2 P2 parameter
* @param data the APDU data
*/
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
this(cla, ins, p1, p2, data, false);
}
/**
* Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
* The LE byte, if sent, is set to 0.
*
* @param cla class byte
* @param ins instruction code
* @param p1 P1 parameter
* @param p2 P2 parameter
* @param data the APDU data
* @param needsLE whether the LE byte should be sent or not
*/
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
this.cla = cla & 0xff;
this.ins = ins & 0xff;
this.p1 = p1 & 0xff;
this.p2 = p2 & 0xff;
this.data = data;
this.needsLE = needsLE;
}
/**
* Serializes the APDU in order to send it to the card.
*
* @return the byte array representation of the APDU
*/
public byte[] serialize() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(this.cla);
out.write(this.ins);
out.write(this.p1);
out.write(this.p2);
out.write(this.data.length);
out.write(this.data);
if (this.needsLE) {
out.write(0); // Response length
}
return out.toByteArray();
}
/**
* Returns the CLA of the APDU
*
* @return the CLA of the APDU
*/
public int getCla() {
return cla;
}
/**
* Returns the INS of the APDU
*
* @return the INS of the APDU
*/
public int getIns() {
return ins;
}
/**
* Returns the P1 of the APDU
*
* @return the P1 of the APDU
*/
public int getP1() {
return p1;
}
/**
* Returns the P2 of the APDU
*
* @return the P2 of the APDU
*/
public int getP2() {
return p2;
}
/**
* Returns the data field of the APDU
*
* @return the data field of the APDU
*/
public byte[] getData() {
return data;
}
/**
* Returns whether LE is sent or not.
*
* @return whether LE is sent or not
*/
public boolean getNeedsLE() {
return this.needsLE;
}
}
@@ -0,0 +1,29 @@
package im.status.keycard.io;
/**
* Exception thrown when the response APDU from the card contains unexpected SW or data.
*/
public class APDUException extends Exception {
public final int sw;
/**
* Creates an exception with SW and message.
*
* @param sw the status word
* @param message a descriptive message of the error
*/
public APDUException(int sw, String message) {
super(message + ", 0x" + String.format("%04X", sw));
this.sw = sw;
}
/**
* Creates an exception with a message.
*
* @param message a descriptive message of the error
*/
public APDUException(String message) {
super(message);
this.sw = 0;
}
}
@@ -0,0 +1,176 @@
package im.status.keycard.io;
/**
* ISO7816-4 APDU response.
*/
public class APDUResponse {
public static final int SW_OK = 0x9000;
public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
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;
private int sw;
private int sw1;
private int sw2;
/**
* Creates an APDU object by parsing the raw response from the card.
*
* @param apdu the raw response from the card.
*/
public APDUResponse(byte[] apdu) {
if (apdu.length < 2) {
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
}
this.apdu = apdu;
this.parse();
}
/**
* Parses the APDU response, separating the response data from SW.
*/
private void parse() {
int length = this.apdu.length;
this.sw1 = this.apdu[length - 2] & 0xff;
this.sw2 = this.apdu[length - 1] & 0xff;
this.sw = (this.sw1 << 8) | this.sw2;
this.data = new byte[length - 2];
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
}
/**
* Returns true if the SW is 0x9000.
*
* @return true if the SW is 0x9000.
*/
public boolean isOK() {
return this.sw == SW_OK;
}
/**
* Asserts that the SW is 0x9000. Throws an exception if it isn't
*
* @return this object, to simplify chaining
* @throws APDUException if the SW is not 0x9000
*/
public APDUResponse checkOK() throws APDUException {
return this.checkSW(SW_OK);
}
/**
* Asserts that the SW is contained in the given list. Throws an exception if it isn't.
*
* @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(int... codes) throws APDUException {
for (int code : codes) {
if (this.sw == code) {
return this;
}
}
switch (this.sw) {
case SW_SECURITY_CONDITION_NOT_SATISFIED:
throw new APDUException(this.sw, "security condition not satisfied");
case SW_AUTHENTICATION_METHOD_BLOCKED:
throw new APDUException(this.sw, "authentication method blocked");
default:
throw new APDUException(this.sw, "Unexpected error SW");
}
}
/**
* 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.
*
* @return the data field of this APDU
*/
public byte[] getData() {
return this.data;
}
/**
* Returns the Status Word.
*
* @return the status word
*/
public int getSw() {
return this.sw;
}
/**
* Returns the SW1 byte
* @return SW1
*/
public int getSw1() {
return this.sw1;
}
/**
* Returns the SW2 byte
* @return SW2
*/
public int getSw2() {
return this.sw2;
}
/**
* Returns the raw unparsed response.
*
* @return raw APDU data
*/
public byte[] getBytes() {
return this.apdu;
}
}
@@ -0,0 +1,35 @@
package im.status.keycard.io;
import java.io.IOException;
/**
* A channel to transcieve ISO7816-4 APDUs.
*/
public interface CardChannel {
/**
* Sends the given C-APDU and returns an R-APDU.
*
* @param cmd the command to send
* @return the card response
* @throws IOException communication error
*/
APDUResponse send(APDUCommand cmd) throws IOException;
/**
* True if connected, false otherwise
* @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,18 @@
package im.status.keycard.io;
/**
* Listener for card connection events.
*/
public interface CardListener {
/**
* Executes when the card channel is connected.
*
* @param channel the connected card channel
*/
void onConnected(CardChannel channel);
/**
* Executes when a previously connected card is disconnected.
*/
void onDisconnected();
}
@@ -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;
}
}
+4 -2
View File
@@ -1,2 +1,4 @@
include ':lib'
include ':demo'
include 'lib'
include 'android'
include 'desktop'
include 'demo-android'